@hema-to/regl-scatterplot 1.14.1-hemato.0
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 +1110 -0
- package/dist/regl-scatterplot.esm.d.ts +126 -0
- package/dist/regl-scatterplot.esm.js +9962 -0
- package/dist/regl-scatterplot.js +9385 -0
- package/dist/regl-scatterplot.min.js +8 -0
- package/dist/types.d.ts +306 -0
- package/package.json +100 -0
- package/src/bg.fs +13 -0
- package/src/bg.vs +16 -0
- package/src/constants.js +189 -0
- package/src/index.js +4654 -0
- package/src/kdbush-class.js +369 -0
- package/src/kdbush-worker.js +20 -0
- package/src/kdbush.js +69 -0
- package/src/lasso-manager/constants.js +7 -0
- package/src/lasso-manager/create-long-press-animations.js +257 -0
- package/src/lasso-manager/create-long-press-elements.js +68 -0
- package/src/lasso-manager/index.js +781 -0
- package/src/lasso-manager/utils.js +40 -0
- package/src/point-simple.fs +10 -0
- package/src/point-update.fs +21 -0
- package/src/point-update.vs +13 -0
- package/src/point.fs +22 -0
- package/src/point.vs +124 -0
- package/src/renderer.js +252 -0
- package/src/spline-curve-worker.js +271 -0
- package/src/spline-curve.js +23 -0
- package/src/types.d.ts +306 -0
- package/src/utils.js +598 -0
package/src/index.js
ADDED
|
@@ -0,0 +1,4654 @@
|
|
|
1
|
+
import {
|
|
2
|
+
hasSameElements,
|
|
3
|
+
identity,
|
|
4
|
+
max as maxArray,
|
|
5
|
+
rangeMap,
|
|
6
|
+
throttleAndDebounce,
|
|
7
|
+
unionIntegers,
|
|
8
|
+
} from '@flekschas/utils';
|
|
9
|
+
import createDom2dCamera from 'dom-2d-camera';
|
|
10
|
+
import earcut from 'earcut';
|
|
11
|
+
import { mat4, vec4 } from 'gl-matrix';
|
|
12
|
+
import createPubSub from 'pub-sub-es';
|
|
13
|
+
import createLine from 'regl-line';
|
|
14
|
+
|
|
15
|
+
import createKdbush from './kdbush.js';
|
|
16
|
+
import createLassoManager from './lasso-manager/index.js';
|
|
17
|
+
import createRenderer from './renderer.js';
|
|
18
|
+
|
|
19
|
+
import BG_FS from './bg.fs';
|
|
20
|
+
import BG_VS from './bg.vs';
|
|
21
|
+
import POINT_SIMPLE_FS from './point-simple.fs';
|
|
22
|
+
import POINT_UPDATE_FS from './point-update.fs';
|
|
23
|
+
import POINT_UPDATE_VS from './point-update.vs';
|
|
24
|
+
import POINT_FS from './point.fs';
|
|
25
|
+
import createVertexShader from './point.vs';
|
|
26
|
+
|
|
27
|
+
import createSplineCurve from './spline-curve.js';
|
|
28
|
+
|
|
29
|
+
import {
|
|
30
|
+
AUTO,
|
|
31
|
+
CATEGORICAL,
|
|
32
|
+
COLOR_ACTIVE_IDX,
|
|
33
|
+
COLOR_BG_IDX,
|
|
34
|
+
COLOR_HOVER_IDX,
|
|
35
|
+
COLOR_NORMAL_IDX,
|
|
36
|
+
COLOR_NUM_STATES,
|
|
37
|
+
CONTINUOUS,
|
|
38
|
+
DEFAULT_ACTION_KEY_MAP,
|
|
39
|
+
DEFAULT_ANNOTATION_HVLINE_LIMIT,
|
|
40
|
+
DEFAULT_ANNOTATION_LINE_COLOR,
|
|
41
|
+
DEFAULT_ANNOTATION_LINE_WIDTH,
|
|
42
|
+
DEFAULT_ANTI_ALIASING,
|
|
43
|
+
DEFAULT_BACKGROUND_IMAGE,
|
|
44
|
+
DEFAULT_CAMERA_IS_FIXED,
|
|
45
|
+
DEFAULT_COLOR_ACTIVE,
|
|
46
|
+
DEFAULT_COLOR_BG,
|
|
47
|
+
DEFAULT_COLOR_BY,
|
|
48
|
+
DEFAULT_COLOR_HOVER,
|
|
49
|
+
DEFAULT_COLOR_NORMAL,
|
|
50
|
+
DEFAULT_DATA_ASPECT_RATIO,
|
|
51
|
+
DEFAULT_DESELECT_ON_DBL_CLICK,
|
|
52
|
+
DEFAULT_DESELECT_ON_ESCAPE,
|
|
53
|
+
DEFAULT_DISTANCE,
|
|
54
|
+
DEFAULT_EASING,
|
|
55
|
+
DEFAULT_HEIGHT,
|
|
56
|
+
DEFAULT_IMAGE_LOAD_TIMEOUT,
|
|
57
|
+
DEFAULT_LASSO_BRUSH_SIZE,
|
|
58
|
+
DEFAULT_LASSO_CLEAR_EVENT,
|
|
59
|
+
DEFAULT_LASSO_COLOR,
|
|
60
|
+
DEFAULT_LASSO_INITIATOR,
|
|
61
|
+
DEFAULT_LASSO_LINE_WIDTH,
|
|
62
|
+
DEFAULT_LASSO_LONG_PRESS_AFTER_EFFECT_TIME,
|
|
63
|
+
DEFAULT_LASSO_LONG_PRESS_EFFECT_DELAY,
|
|
64
|
+
DEFAULT_LASSO_LONG_PRESS_REVERT_EFFECT_TIME,
|
|
65
|
+
DEFAULT_LASSO_LONG_PRESS_TIME,
|
|
66
|
+
DEFAULT_LASSO_MIN_DELAY,
|
|
67
|
+
DEFAULT_LASSO_MIN_DIST,
|
|
68
|
+
DEFAULT_LASSO_MODE,
|
|
69
|
+
DEFAULT_LASSO_ON_LONG_PRESS,
|
|
70
|
+
DEFAULT_LASSO_TYPE,
|
|
71
|
+
DEFAULT_MOUSE_MODE,
|
|
72
|
+
DEFAULT_OPACITY_BY,
|
|
73
|
+
DEFAULT_OPACITY_BY_DENSITY_DEBOUNCE_TIME,
|
|
74
|
+
DEFAULT_OPACITY_BY_DENSITY_FILL,
|
|
75
|
+
DEFAULT_OPACITY_INACTIVE_MAX,
|
|
76
|
+
DEFAULT_OPACITY_INACTIVE_SCALE,
|
|
77
|
+
DEFAULT_PERFORMANCE_MODE,
|
|
78
|
+
DEFAULT_PIXEL_ALIGNED,
|
|
79
|
+
DEFAULT_POINT_CONNECTION_COLOR_ACTIVE,
|
|
80
|
+
DEFAULT_POINT_CONNECTION_COLOR_BY,
|
|
81
|
+
DEFAULT_POINT_CONNECTION_COLOR_HOVER,
|
|
82
|
+
DEFAULT_POINT_CONNECTION_COLOR_NORMAL,
|
|
83
|
+
DEFAULT_POINT_CONNECTION_INT_POINTS_TOLERANCE,
|
|
84
|
+
DEFAULT_POINT_CONNECTION_MAX_INT_POINTS_PER_SEGMENT,
|
|
85
|
+
DEFAULT_POINT_CONNECTION_OPACITY,
|
|
86
|
+
DEFAULT_POINT_CONNECTION_OPACITY_ACTIVE,
|
|
87
|
+
DEFAULT_POINT_CONNECTION_OPACITY_BY,
|
|
88
|
+
DEFAULT_POINT_CONNECTION_SIZE,
|
|
89
|
+
DEFAULT_POINT_CONNECTION_SIZE_ACTIVE,
|
|
90
|
+
DEFAULT_POINT_CONNECTION_SIZE_BY,
|
|
91
|
+
DEFAULT_POINT_OUTLINE_WIDTH,
|
|
92
|
+
DEFAULT_POINT_SCALE_MODE,
|
|
93
|
+
DEFAULT_POINT_SIZE,
|
|
94
|
+
DEFAULT_POINT_SIZE_MOUSE_DETECTION,
|
|
95
|
+
DEFAULT_POINT_SIZE_SELECTED,
|
|
96
|
+
DEFAULT_RETICLE_COLOR,
|
|
97
|
+
DEFAULT_ROTATION,
|
|
98
|
+
DEFAULT_SHOW_POINT_CONNECTIONS,
|
|
99
|
+
DEFAULT_SHOW_RETICLE,
|
|
100
|
+
DEFAULT_SIZE_BY,
|
|
101
|
+
DEFAULT_SPATIAL_INDEX_USE_WORKER,
|
|
102
|
+
DEFAULT_TARGET,
|
|
103
|
+
DEFAULT_VIEW,
|
|
104
|
+
DEFAULT_WIDTH,
|
|
105
|
+
EASING_FNS,
|
|
106
|
+
ERROR_INSTANCE_IS_DESTROYED,
|
|
107
|
+
ERROR_IS_DRAWING,
|
|
108
|
+
ERROR_POINTS_NOT_DRAWN,
|
|
109
|
+
FLOAT_BYTES,
|
|
110
|
+
KEYS,
|
|
111
|
+
KEY_ACTIONS,
|
|
112
|
+
KEY_ACTION_INTERSECT,
|
|
113
|
+
KEY_ACTION_LASSO,
|
|
114
|
+
KEY_ACTION_MERGE,
|
|
115
|
+
KEY_ACTION_REMOVE,
|
|
116
|
+
KEY_ACTION_ROTATE,
|
|
117
|
+
KEY_ALT,
|
|
118
|
+
KEY_CMD,
|
|
119
|
+
KEY_CTRL,
|
|
120
|
+
KEY_META,
|
|
121
|
+
KEY_SHIFT,
|
|
122
|
+
LASSO_BRUSH_MIN_MIN_DIST,
|
|
123
|
+
LASSO_CLEAR_EVENTS,
|
|
124
|
+
LASSO_CLEAR_ON_DESELECT,
|
|
125
|
+
LASSO_CLEAR_ON_END,
|
|
126
|
+
LONG_CLICK_TIME,
|
|
127
|
+
MIN_POINT_SIZE,
|
|
128
|
+
MOUSE_MODES,
|
|
129
|
+
MOUSE_MODE_LASSO,
|
|
130
|
+
MOUSE_MODE_PANZOOM,
|
|
131
|
+
MOUSE_MODE_ROTATE,
|
|
132
|
+
SINGLE_CLICK_DELAY,
|
|
133
|
+
SKIP_DEPRECATION_VALUE_TRANSLATION,
|
|
134
|
+
VALUE_ZW_DATA_TYPES,
|
|
135
|
+
W_NAMES,
|
|
136
|
+
Z_NAMES,
|
|
137
|
+
} from './constants.js';
|
|
138
|
+
|
|
139
|
+
import {
|
|
140
|
+
checkReglExtensions as checkSupport,
|
|
141
|
+
clip,
|
|
142
|
+
createRegl,
|
|
143
|
+
createTextureFromUrl,
|
|
144
|
+
dist,
|
|
145
|
+
flipObj,
|
|
146
|
+
getBBox,
|
|
147
|
+
insertionSort,
|
|
148
|
+
isConditionalArray,
|
|
149
|
+
isDomRect,
|
|
150
|
+
isHorizontalLine,
|
|
151
|
+
isMultipleColors,
|
|
152
|
+
isPointInPolygon,
|
|
153
|
+
isPolygon,
|
|
154
|
+
isPositiveNumber,
|
|
155
|
+
isRect,
|
|
156
|
+
isSameRgbas,
|
|
157
|
+
isStrictlyPositiveNumber,
|
|
158
|
+
isString,
|
|
159
|
+
isValidBBox,
|
|
160
|
+
isVerticalLine,
|
|
161
|
+
limit,
|
|
162
|
+
max,
|
|
163
|
+
min,
|
|
164
|
+
rgbBrightness,
|
|
165
|
+
toArrayOrientedPoints,
|
|
166
|
+
toRgba,
|
|
167
|
+
} from './utils.js';
|
|
168
|
+
|
|
169
|
+
import { version } from '../package.json';
|
|
170
|
+
|
|
171
|
+
const deprecations = {
|
|
172
|
+
showRecticle: {
|
|
173
|
+
replacement: 'showReticle',
|
|
174
|
+
removalVersion: '2',
|
|
175
|
+
translation: identity,
|
|
176
|
+
},
|
|
177
|
+
recticleColor: {
|
|
178
|
+
replacement: 'reticleColor',
|
|
179
|
+
removalVersion: '2',
|
|
180
|
+
translation: identity,
|
|
181
|
+
},
|
|
182
|
+
keyMap: {
|
|
183
|
+
replacement: 'actionKeyMap',
|
|
184
|
+
removalVersion: '2',
|
|
185
|
+
translation: flipObj,
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const checkDeprecations = (properties) => {
|
|
190
|
+
const deprecatedProps = Object.keys(properties).filter(
|
|
191
|
+
(prop) => deprecations[prop],
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
for (const prop of deprecatedProps) {
|
|
195
|
+
const { replacement, removalVersion, translation } = deprecations[prop];
|
|
196
|
+
// biome-ignore lint/suspicious/noConsole: This is a legitimately useful warning
|
|
197
|
+
console.warn(
|
|
198
|
+
`regl-scatterplot: the "${prop}" property is deprecated and will be removed in v${removalVersion}. Please use "${replacement}" instead.`,
|
|
199
|
+
);
|
|
200
|
+
properties[deprecations[prop].replacement] =
|
|
201
|
+
properties[prop] !== SKIP_DEPRECATION_VALUE_TRANSLATION
|
|
202
|
+
? translation(properties[prop])
|
|
203
|
+
: properties[prop];
|
|
204
|
+
delete properties[prop];
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return properties;
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const getEncodingType = (
|
|
211
|
+
type,
|
|
212
|
+
defaultValue,
|
|
213
|
+
{ allowSegment = false, allowDensity = false, allowInherit = false } = {},
|
|
214
|
+
) => {
|
|
215
|
+
// Z refers to the 3rd component of the RGBA value
|
|
216
|
+
if (Z_NAMES.has(type)) {
|
|
217
|
+
return 'valueZ';
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// W refers to the 4th component of the RGBA value
|
|
221
|
+
if (W_NAMES.has(type)) {
|
|
222
|
+
return 'valueW';
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (type === 'segment') {
|
|
226
|
+
return allowSegment ? 'segment' : defaultValue;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (type === 'density') {
|
|
230
|
+
return allowDensity ? 'density' : defaultValue;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (type === 'inherit') {
|
|
234
|
+
return allowInherit ? 'inherit' : defaultValue;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return defaultValue;
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const getEncodingIdx = (type) => {
|
|
241
|
+
switch (type) {
|
|
242
|
+
case 'valueZ':
|
|
243
|
+
return 2;
|
|
244
|
+
|
|
245
|
+
case 'valueW':
|
|
246
|
+
return 3;
|
|
247
|
+
|
|
248
|
+
default:
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const createScatterplot = (
|
|
254
|
+
/** @type {Partial<import('./types').Properties>} */ initialProperties = {},
|
|
255
|
+
) => {
|
|
256
|
+
/** @type {import('pub-sub-es').PubSub<import('./types').Events>} */
|
|
257
|
+
const pubSub = createPubSub({
|
|
258
|
+
async: !initialProperties.syncEvents,
|
|
259
|
+
caseInsensitive: true,
|
|
260
|
+
});
|
|
261
|
+
const scratch = new Float32Array(16);
|
|
262
|
+
const pvm = new Float32Array(16);
|
|
263
|
+
const mousePosition = [0, 0];
|
|
264
|
+
|
|
265
|
+
checkDeprecations(initialProperties);
|
|
266
|
+
|
|
267
|
+
let {
|
|
268
|
+
renderer,
|
|
269
|
+
antiAliasing = DEFAULT_ANTI_ALIASING,
|
|
270
|
+
pixelAligned = DEFAULT_PIXEL_ALIGNED,
|
|
271
|
+
backgroundColor = DEFAULT_COLOR_BG,
|
|
272
|
+
backgroundImage = DEFAULT_BACKGROUND_IMAGE,
|
|
273
|
+
canvas = document.createElement('canvas'),
|
|
274
|
+
colorBy = DEFAULT_COLOR_BY,
|
|
275
|
+
deselectOnDblClick = DEFAULT_DESELECT_ON_DBL_CLICK,
|
|
276
|
+
deselectOnEscape = DEFAULT_DESELECT_ON_ESCAPE,
|
|
277
|
+
lassoColor = DEFAULT_LASSO_COLOR,
|
|
278
|
+
lassoLineWidth = DEFAULT_LASSO_LINE_WIDTH,
|
|
279
|
+
lassoMinDelay = DEFAULT_LASSO_MIN_DELAY,
|
|
280
|
+
lassoMinDist = DEFAULT_LASSO_MIN_DIST,
|
|
281
|
+
lassoMode = DEFAULT_LASSO_MODE,
|
|
282
|
+
lassoClearEvent = DEFAULT_LASSO_CLEAR_EVENT,
|
|
283
|
+
lassoInitiator = DEFAULT_LASSO_INITIATOR,
|
|
284
|
+
lassoInitiatorParentElement = document.body,
|
|
285
|
+
lassoLongPressIndicatorParentElement = document.body,
|
|
286
|
+
lassoOnLongPress = DEFAULT_LASSO_ON_LONG_PRESS,
|
|
287
|
+
lassoLongPressTime = DEFAULT_LASSO_LONG_PRESS_TIME,
|
|
288
|
+
lassoLongPressAfterEffectTime = DEFAULT_LASSO_LONG_PRESS_AFTER_EFFECT_TIME,
|
|
289
|
+
lassoLongPressEffectDelay = DEFAULT_LASSO_LONG_PRESS_EFFECT_DELAY,
|
|
290
|
+
lassoLongPressRevertEffectTime = DEFAULT_LASSO_LONG_PRESS_REVERT_EFFECT_TIME,
|
|
291
|
+
lassoType = DEFAULT_LASSO_TYPE,
|
|
292
|
+
lassoBrushSize = DEFAULT_LASSO_BRUSH_SIZE,
|
|
293
|
+
actionKeyMap = DEFAULT_ACTION_KEY_MAP,
|
|
294
|
+
mouseMode = DEFAULT_MOUSE_MODE,
|
|
295
|
+
showReticle = DEFAULT_SHOW_RETICLE,
|
|
296
|
+
reticleColor = DEFAULT_RETICLE_COLOR,
|
|
297
|
+
pointColor = DEFAULT_COLOR_NORMAL,
|
|
298
|
+
pointColorActive = DEFAULT_COLOR_ACTIVE,
|
|
299
|
+
pointColorHover = DEFAULT_COLOR_HOVER,
|
|
300
|
+
showPointConnections = DEFAULT_SHOW_POINT_CONNECTIONS,
|
|
301
|
+
pointConnectionColor = DEFAULT_POINT_CONNECTION_COLOR_NORMAL,
|
|
302
|
+
pointConnectionColorActive = DEFAULT_POINT_CONNECTION_COLOR_ACTIVE,
|
|
303
|
+
pointConnectionColorHover = DEFAULT_POINT_CONNECTION_COLOR_HOVER,
|
|
304
|
+
pointConnectionColorBy = DEFAULT_POINT_CONNECTION_COLOR_BY,
|
|
305
|
+
pointConnectionOpacity = DEFAULT_POINT_CONNECTION_OPACITY,
|
|
306
|
+
pointConnectionOpacityBy = DEFAULT_POINT_CONNECTION_OPACITY_BY,
|
|
307
|
+
pointConnectionOpacityActive = DEFAULT_POINT_CONNECTION_OPACITY_ACTIVE,
|
|
308
|
+
pointConnectionSize = DEFAULT_POINT_CONNECTION_SIZE,
|
|
309
|
+
pointConnectionSizeActive = DEFAULT_POINT_CONNECTION_SIZE_ACTIVE,
|
|
310
|
+
pointConnectionSizeBy = DEFAULT_POINT_CONNECTION_SIZE_BY,
|
|
311
|
+
pointConnectionMaxIntPointsPerSegment = DEFAULT_POINT_CONNECTION_MAX_INT_POINTS_PER_SEGMENT,
|
|
312
|
+
pointConnectionTolerance = DEFAULT_POINT_CONNECTION_INT_POINTS_TOLERANCE,
|
|
313
|
+
pointSize = DEFAULT_POINT_SIZE,
|
|
314
|
+
pointSizeSelected = DEFAULT_POINT_SIZE_SELECTED,
|
|
315
|
+
pointSizeMouseDetection = DEFAULT_POINT_SIZE_MOUSE_DETECTION,
|
|
316
|
+
pointOutlineWidth = DEFAULT_POINT_OUTLINE_WIDTH,
|
|
317
|
+
opacity = AUTO,
|
|
318
|
+
opacityBy = DEFAULT_OPACITY_BY,
|
|
319
|
+
opacityByDensityFill = DEFAULT_OPACITY_BY_DENSITY_FILL,
|
|
320
|
+
opacityInactiveMax = DEFAULT_OPACITY_INACTIVE_MAX,
|
|
321
|
+
opacityInactiveScale = DEFAULT_OPACITY_INACTIVE_SCALE,
|
|
322
|
+
sizeBy = DEFAULT_SIZE_BY,
|
|
323
|
+
pointScaleMode = DEFAULT_POINT_SCALE_MODE,
|
|
324
|
+
height = DEFAULT_HEIGHT,
|
|
325
|
+
width = DEFAULT_WIDTH,
|
|
326
|
+
annotationLineColor = DEFAULT_ANNOTATION_LINE_COLOR,
|
|
327
|
+
annotationLineWidth = DEFAULT_ANNOTATION_LINE_WIDTH,
|
|
328
|
+
annotationHVLineLimit = DEFAULT_ANNOTATION_HVLINE_LIMIT,
|
|
329
|
+
cameraIsFixed = DEFAULT_CAMERA_IS_FIXED,
|
|
330
|
+
} = initialProperties;
|
|
331
|
+
|
|
332
|
+
let currentWidth = width === AUTO ? 1 : width;
|
|
333
|
+
let currentHeight = height === AUTO ? 1 : height;
|
|
334
|
+
|
|
335
|
+
// The following properties cannot be changed after the initialization
|
|
336
|
+
const {
|
|
337
|
+
performanceMode = DEFAULT_PERFORMANCE_MODE,
|
|
338
|
+
opacityByDensityDebounceTime = DEFAULT_OPACITY_BY_DENSITY_DEBOUNCE_TIME,
|
|
339
|
+
spatialIndexUseWorker = DEFAULT_SPATIAL_INDEX_USE_WORKER,
|
|
340
|
+
} = initialProperties;
|
|
341
|
+
|
|
342
|
+
const renderPointsAsSquares = Boolean(
|
|
343
|
+
initialProperties.renderPointsAsSquares || performanceMode,
|
|
344
|
+
);
|
|
345
|
+
const disableAlphaBlending = Boolean(
|
|
346
|
+
initialProperties.disableAlphaBlending || performanceMode,
|
|
347
|
+
);
|
|
348
|
+
|
|
349
|
+
mouseMode = limit(MOUSE_MODES, MOUSE_MODE_PANZOOM)(mouseMode);
|
|
350
|
+
|
|
351
|
+
if (!renderer) {
|
|
352
|
+
renderer = createRenderer({
|
|
353
|
+
regl: initialProperties.regl,
|
|
354
|
+
gamma: initialProperties.gamma,
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
backgroundColor = toRgba(backgroundColor, true);
|
|
359
|
+
lassoColor = toRgba(lassoColor, true);
|
|
360
|
+
reticleColor = toRgba(reticleColor, true);
|
|
361
|
+
|
|
362
|
+
let isDrawing = false;
|
|
363
|
+
let isDestroyed = false;
|
|
364
|
+
let backgroundColorBrightness = rgbBrightness(backgroundColor);
|
|
365
|
+
let camera;
|
|
366
|
+
/** @type {ReturnType<createLine>} */
|
|
367
|
+
let lasso;
|
|
368
|
+
/** @type {ReturnType<createLine>} */
|
|
369
|
+
let annotations;
|
|
370
|
+
let mouseDown = false;
|
|
371
|
+
let mouseDownTime = null;
|
|
372
|
+
let mouseDownPosition = [0, 0];
|
|
373
|
+
let mouseDownTimeout = -1;
|
|
374
|
+
/** @type{number[]} */
|
|
375
|
+
let selectedPoints = [];
|
|
376
|
+
/** @type{Set<number>} */
|
|
377
|
+
const selectedPointsSet = new Set();
|
|
378
|
+
/** @type{Set<number>} */
|
|
379
|
+
const selectedPointsConnectionSet = new Set();
|
|
380
|
+
let isPointsFiltered = false;
|
|
381
|
+
/** @type{Set<number>} */
|
|
382
|
+
const filteredPointsSet = new Set();
|
|
383
|
+
let points = [];
|
|
384
|
+
let numPoints = 0;
|
|
385
|
+
let numPointsInView = 0;
|
|
386
|
+
let lassoActive = false;
|
|
387
|
+
let lassoPointsCurr = [];
|
|
388
|
+
let spatialIndex;
|
|
389
|
+
let viewAspectRatio;
|
|
390
|
+
let dataAspectRatio =
|
|
391
|
+
initialProperties.aspectRatio || DEFAULT_DATA_ASPECT_RATIO;
|
|
392
|
+
let projectionLocal;
|
|
393
|
+
let projection;
|
|
394
|
+
let model;
|
|
395
|
+
let pointConnections;
|
|
396
|
+
let pointConnectionMap;
|
|
397
|
+
let computingPointConnectionCurves;
|
|
398
|
+
// biome-ignore lint/style/useNamingConvention: HLine stands for HorizontalLine
|
|
399
|
+
let reticleHLine;
|
|
400
|
+
// biome-ignore lint/style/useNamingConvention: VLine stands for VerticalLine
|
|
401
|
+
let reticleVLine;
|
|
402
|
+
let computedPointSizeMouseDetection;
|
|
403
|
+
let lassoInitiatorTimeout;
|
|
404
|
+
let topRightNdc;
|
|
405
|
+
let bottomLeftNdc;
|
|
406
|
+
let preventEventView = false;
|
|
407
|
+
let draw = true;
|
|
408
|
+
let drawReticleOnce = false;
|
|
409
|
+
let canvasObserver;
|
|
410
|
+
|
|
411
|
+
pointColor = isMultipleColors(pointColor) ? [...pointColor] : [pointColor];
|
|
412
|
+
pointColorActive = isMultipleColors(pointColorActive)
|
|
413
|
+
? [...pointColorActive]
|
|
414
|
+
: [pointColorActive];
|
|
415
|
+
pointColorHover = isMultipleColors(pointColorHover)
|
|
416
|
+
? [...pointColorHover]
|
|
417
|
+
: [pointColorHover];
|
|
418
|
+
|
|
419
|
+
pointColor = pointColor.map((color) => toRgba(color, true));
|
|
420
|
+
pointColorActive = pointColorActive.map((color) => toRgba(color, true));
|
|
421
|
+
pointColorHover = pointColorHover.map((color) => toRgba(color, true));
|
|
422
|
+
|
|
423
|
+
opacity =
|
|
424
|
+
!Array.isArray(opacity) && Number.isNaN(+opacity)
|
|
425
|
+
? pointColor[0][3]
|
|
426
|
+
: opacity;
|
|
427
|
+
opacity = isConditionalArray(opacity, isPositiveNumber, {
|
|
428
|
+
minLength: 1,
|
|
429
|
+
})
|
|
430
|
+
? [...opacity]
|
|
431
|
+
: [opacity];
|
|
432
|
+
|
|
433
|
+
pointSize = isConditionalArray(pointSize, isPositiveNumber, {
|
|
434
|
+
minLength: 1,
|
|
435
|
+
})
|
|
436
|
+
? [...pointSize]
|
|
437
|
+
: [pointSize];
|
|
438
|
+
|
|
439
|
+
let minPointScale = MIN_POINT_SIZE / pointSize[0];
|
|
440
|
+
|
|
441
|
+
if (pointConnectionColor === 'inherit') {
|
|
442
|
+
pointConnectionColor = [...pointColor];
|
|
443
|
+
} else {
|
|
444
|
+
pointConnectionColor = isMultipleColors(pointConnectionColor)
|
|
445
|
+
? [...pointConnectionColor]
|
|
446
|
+
: [pointConnectionColor];
|
|
447
|
+
pointConnectionColor = pointConnectionColor.map((color) =>
|
|
448
|
+
toRgba(color, true),
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if (pointConnectionColorActive === 'inherit') {
|
|
453
|
+
pointConnectionColorActive = [...pointColorActive];
|
|
454
|
+
} else {
|
|
455
|
+
pointConnectionColorActive = isMultipleColors(pointConnectionColorActive)
|
|
456
|
+
? [...pointConnectionColorActive]
|
|
457
|
+
: [pointConnectionColorActive];
|
|
458
|
+
pointConnectionColorActive = pointConnectionColorActive.map((color) =>
|
|
459
|
+
toRgba(color, true),
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (pointConnectionColorHover === 'inherit') {
|
|
464
|
+
pointConnectionColorHover = [...pointColorHover];
|
|
465
|
+
} else {
|
|
466
|
+
pointConnectionColorHover = isMultipleColors(pointConnectionColorHover)
|
|
467
|
+
? [...pointConnectionColorHover]
|
|
468
|
+
: [pointConnectionColorHover];
|
|
469
|
+
pointConnectionColorHover = pointConnectionColorHover.map((color) =>
|
|
470
|
+
toRgba(color, true),
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
if (pointConnectionOpacity === 'inherit') {
|
|
475
|
+
pointConnectionOpacity = [...opacity];
|
|
476
|
+
} else {
|
|
477
|
+
pointConnectionOpacity = isConditionalArray(
|
|
478
|
+
pointConnectionOpacity,
|
|
479
|
+
isPositiveNumber,
|
|
480
|
+
{
|
|
481
|
+
minLength: 1,
|
|
482
|
+
},
|
|
483
|
+
)
|
|
484
|
+
? [...pointConnectionOpacity]
|
|
485
|
+
: [pointConnectionOpacity];
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if (pointConnectionSize === 'inherit') {
|
|
489
|
+
pointConnectionSize = [...pointSize];
|
|
490
|
+
} else {
|
|
491
|
+
pointConnectionSize = isConditionalArray(
|
|
492
|
+
pointConnectionSize,
|
|
493
|
+
isPositiveNumber,
|
|
494
|
+
{
|
|
495
|
+
minLength: 1,
|
|
496
|
+
},
|
|
497
|
+
)
|
|
498
|
+
? [...pointConnectionSize]
|
|
499
|
+
: [pointConnectionSize];
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
colorBy = getEncodingType(colorBy, DEFAULT_COLOR_BY);
|
|
503
|
+
opacityBy = getEncodingType(opacityBy, DEFAULT_OPACITY_BY, {
|
|
504
|
+
allowDensity: true,
|
|
505
|
+
});
|
|
506
|
+
sizeBy = getEncodingType(sizeBy, DEFAULT_SIZE_BY);
|
|
507
|
+
|
|
508
|
+
pointConnectionColorBy = getEncodingType(
|
|
509
|
+
pointConnectionColorBy,
|
|
510
|
+
DEFAULT_POINT_CONNECTION_COLOR_BY,
|
|
511
|
+
{ allowSegment: true, allowInherit: true },
|
|
512
|
+
);
|
|
513
|
+
pointConnectionOpacityBy = getEncodingType(
|
|
514
|
+
pointConnectionOpacityBy,
|
|
515
|
+
DEFAULT_POINT_CONNECTION_OPACITY_BY,
|
|
516
|
+
{ allowSegment: true },
|
|
517
|
+
);
|
|
518
|
+
pointConnectionSizeBy = getEncodingType(
|
|
519
|
+
pointConnectionSizeBy,
|
|
520
|
+
DEFAULT_POINT_CONNECTION_SIZE_BY,
|
|
521
|
+
{ allowSegment: true },
|
|
522
|
+
);
|
|
523
|
+
|
|
524
|
+
let stateTex; // Stores the point texture holding x, y, category, and value
|
|
525
|
+
let prevStateTex; // Stores the previous point texture. Used for transitions
|
|
526
|
+
let tmpStateTex; // Stores a temporary point texture. Used for transitions
|
|
527
|
+
let tmpStateBuffer; // Temporary frame buffer
|
|
528
|
+
let stateTexRes = 0; // Width and height of the texture
|
|
529
|
+
let stateTexEps = 0; // Half a texel
|
|
530
|
+
let normalPointsIndexBuffer; // Buffer holding the indices pointing to the correct texel
|
|
531
|
+
let selectedPointsIndexBuffer; // Used for pointing to the selected texels
|
|
532
|
+
let hoveredPointIndexBuffer; // Used for pointing to the hovered texels
|
|
533
|
+
|
|
534
|
+
let cameraZoomTargetStart; // Stores the start (i.e., current) camera target for zooming
|
|
535
|
+
let cameraZoomTargetEnd; // Stores the end camera target for zooming
|
|
536
|
+
let cameraZoomDistanceStart; // Stores the start camera distance for zooming
|
|
537
|
+
let cameraZoomDistanceEnd; // Stores the end camera distance for zooming
|
|
538
|
+
|
|
539
|
+
let isTransitioning = false;
|
|
540
|
+
let transitionStartTime = null;
|
|
541
|
+
let transitionDuration;
|
|
542
|
+
let transitionEasing;
|
|
543
|
+
let preTransitionShowReticle = showReticle;
|
|
544
|
+
|
|
545
|
+
let colorTex; // Stores the point color texture
|
|
546
|
+
let colorTexRes = 0; // Width and height of the texture
|
|
547
|
+
let encodingTex; // Stores the point sizes and opacity values
|
|
548
|
+
let encodingTexRes = 0; // Width and height of the texture
|
|
549
|
+
|
|
550
|
+
let isViewChanged = false;
|
|
551
|
+
let isPointsDrawn = false;
|
|
552
|
+
let isAnnotationsDrawn = false;
|
|
553
|
+
let isMouseOverCanvasChecked = false;
|
|
554
|
+
|
|
555
|
+
// biome-ignore lint/style/useNamingConvention: ZDate is not one word
|
|
556
|
+
let valueZDataType = CATEGORICAL;
|
|
557
|
+
// biome-ignore lint/style/useNamingConvention: WDate is not one word
|
|
558
|
+
let valueWDataType = CATEGORICAL;
|
|
559
|
+
|
|
560
|
+
/** @type{number|undefined} */
|
|
561
|
+
let hoveredPoint;
|
|
562
|
+
let isMouseInCanvas = false;
|
|
563
|
+
|
|
564
|
+
let xScale = initialProperties.xScale || null;
|
|
565
|
+
let yScale = initialProperties.yScale || null;
|
|
566
|
+
let xDomainStart = 0;
|
|
567
|
+
let xDomainSize = 0;
|
|
568
|
+
let yDomainStart = 0;
|
|
569
|
+
let yDomainSize = 0;
|
|
570
|
+
if (xScale) {
|
|
571
|
+
xDomainStart = xScale.domain()[0];
|
|
572
|
+
xDomainSize = xScale.domain()[1] - xScale.domain()[0];
|
|
573
|
+
xScale.range([0, currentWidth]);
|
|
574
|
+
}
|
|
575
|
+
if (yScale) {
|
|
576
|
+
yDomainStart = yScale.domain()[0];
|
|
577
|
+
yDomainSize = yScale.domain()[1] - yScale.domain()[0];
|
|
578
|
+
yScale.range([currentHeight, 0]);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
const getNdcX = (x) => -1 + (x / currentWidth) * 2;
|
|
582
|
+
const getNdcY = (y) => 1 + (y / currentHeight) * -2;
|
|
583
|
+
|
|
584
|
+
// Get relative WebGL position
|
|
585
|
+
const getMouseGlPos = () => [
|
|
586
|
+
getNdcX(mousePosition[0]),
|
|
587
|
+
getNdcY(mousePosition[1]),
|
|
588
|
+
];
|
|
589
|
+
|
|
590
|
+
const getScatterGlPos = (xGl, yGl) => {
|
|
591
|
+
// Homogeneous vector
|
|
592
|
+
const v = [xGl, yGl, 1, 1];
|
|
593
|
+
|
|
594
|
+
// projection^-1 * view^-1 * model^-1 is the same as
|
|
595
|
+
// model * view^-1 * projection
|
|
596
|
+
const mvp = mat4.invert(
|
|
597
|
+
scratch,
|
|
598
|
+
mat4.multiply(
|
|
599
|
+
scratch,
|
|
600
|
+
projectionLocal,
|
|
601
|
+
mat4.multiply(scratch, camera.view, model),
|
|
602
|
+
),
|
|
603
|
+
);
|
|
604
|
+
|
|
605
|
+
// Translate vector
|
|
606
|
+
vec4.transformMat4(v, v, mvp);
|
|
607
|
+
|
|
608
|
+
return v.slice(0, 2);
|
|
609
|
+
};
|
|
610
|
+
|
|
611
|
+
const getPointSizeNdc = (pointSizeIncrease = 0) => {
|
|
612
|
+
const pointScale = getPointScale();
|
|
613
|
+
|
|
614
|
+
// The height of the view in normalized device coordinates
|
|
615
|
+
const heightNdc = topRightNdc[1] - bottomLeftNdc[1];
|
|
616
|
+
// The size of a pixel in the current view in normalized device coordinates
|
|
617
|
+
const pxNdc = heightNdc / canvas.height;
|
|
618
|
+
// The scaled point size in normalized device coordinates
|
|
619
|
+
return (
|
|
620
|
+
(computedPointSizeMouseDetection * pointScale + pointSizeIncrease) * pxNdc
|
|
621
|
+
);
|
|
622
|
+
};
|
|
623
|
+
|
|
624
|
+
const getPoints = () => {
|
|
625
|
+
if (isPointsFiltered) {
|
|
626
|
+
return points.filter((_, i) => filteredPointsSet.has(i));
|
|
627
|
+
}
|
|
628
|
+
return points;
|
|
629
|
+
};
|
|
630
|
+
|
|
631
|
+
// biome-ignore lint/style/useNamingConvention: BBox stands for BoundingBox
|
|
632
|
+
const getPointsInBBox = (x0, y0, x1, y1) => {
|
|
633
|
+
// biome-ignore lint/style/useNamingConvention: BBox stands for BoundingBox
|
|
634
|
+
const pointsInBBox = spatialIndex.range(x0, y0, x1, y1);
|
|
635
|
+
if (isPointsFiltered) {
|
|
636
|
+
return pointsInBBox.filter((i) => filteredPointsSet.has(i));
|
|
637
|
+
}
|
|
638
|
+
return pointsInBBox;
|
|
639
|
+
};
|
|
640
|
+
|
|
641
|
+
const raycast = () => {
|
|
642
|
+
const [xGl, yGl] = getMouseGlPos();
|
|
643
|
+
const [xNdc, yNdc] = getScatterGlPos(xGl, yGl);
|
|
644
|
+
|
|
645
|
+
const pointSizeNdc = getPointSizeNdc(4);
|
|
646
|
+
|
|
647
|
+
// Get all points within a close range
|
|
648
|
+
// biome-ignore lint/style/useNamingConvention: BBox stands for BoundingBox
|
|
649
|
+
const pointsInBBox = getPointsInBBox(
|
|
650
|
+
xNdc - pointSizeNdc,
|
|
651
|
+
yNdc - pointSizeNdc,
|
|
652
|
+
xNdc + pointSizeNdc,
|
|
653
|
+
yNdc + pointSizeNdc,
|
|
654
|
+
);
|
|
655
|
+
|
|
656
|
+
// Find the closest point
|
|
657
|
+
let minDist = pointSizeNdc;
|
|
658
|
+
let clostestPointIdx = -1;
|
|
659
|
+
for (const pointIdx of pointsInBBox) {
|
|
660
|
+
const [ptX, ptY] = points[pointIdx];
|
|
661
|
+
const d = dist(ptX, ptY, xNdc, yNdc);
|
|
662
|
+
if (d < minDist) {
|
|
663
|
+
minDist = d;
|
|
664
|
+
clostestPointIdx = pointIdx;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
return clostestPointIdx;
|
|
669
|
+
};
|
|
670
|
+
|
|
671
|
+
const lassoExtend = (lassoPoints, lassoPointsFlat) => {
|
|
672
|
+
lassoPointsCurr = lassoPoints;
|
|
673
|
+
lasso.setPoints(lassoPointsFlat);
|
|
674
|
+
pubSub.publish('lassoExtend', { coordinates: lassoPoints });
|
|
675
|
+
};
|
|
676
|
+
|
|
677
|
+
const findPointsInLasso = (lassoPolygon) => {
|
|
678
|
+
// get the bounding box of the lasso selection...
|
|
679
|
+
const bBox = getBBox(lassoPolygon);
|
|
680
|
+
|
|
681
|
+
if (!isValidBBox(bBox)) {
|
|
682
|
+
return [];
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// ...to efficiently preselect potentially selected points
|
|
686
|
+
// biome-ignore lint/style/useNamingConvention: BBox stands for BoundingBox
|
|
687
|
+
const pointsInBBox = getPointsInBBox(...bBox);
|
|
688
|
+
// next we test each point in the bounding box if it is in the polygon too
|
|
689
|
+
const pointsInPolygon = [];
|
|
690
|
+
for (const pointIdx of pointsInBBox) {
|
|
691
|
+
if (isPointInPolygon(lassoPolygon, points[pointIdx])) {
|
|
692
|
+
pointsInPolygon.push(pointIdx);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
return pointsInPolygon;
|
|
697
|
+
};
|
|
698
|
+
|
|
699
|
+
const lassoClear = () => {
|
|
700
|
+
lassoPointsCurr = [];
|
|
701
|
+
if (lasso) {
|
|
702
|
+
lasso.clear();
|
|
703
|
+
}
|
|
704
|
+
};
|
|
705
|
+
|
|
706
|
+
const hasPointConnections = (point) => point && point.length > 4;
|
|
707
|
+
|
|
708
|
+
const setPointConnectionColorState = (pointIdxs, stateIndex) => {
|
|
709
|
+
if (
|
|
710
|
+
computingPointConnectionCurves ||
|
|
711
|
+
!showPointConnections ||
|
|
712
|
+
!hasPointConnections(points[pointIdxs[0]])
|
|
713
|
+
) {
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
const isNormal = stateIndex === 0;
|
|
718
|
+
const lineIdCacher =
|
|
719
|
+
stateIndex === 1
|
|
720
|
+
? (lineId) => selectedPointsConnectionSet.add(lineId)
|
|
721
|
+
: identity;
|
|
722
|
+
|
|
723
|
+
// Get line IDs
|
|
724
|
+
const lineIds = Object.keys(
|
|
725
|
+
pointIdxs.reduce((ids, pointIdx) => {
|
|
726
|
+
const point = points[pointIdx];
|
|
727
|
+
const isStruct = Array.isArray(point[4]);
|
|
728
|
+
const lineId = isStruct ? point[4][0] : point[4];
|
|
729
|
+
|
|
730
|
+
ids[lineId] = true;
|
|
731
|
+
|
|
732
|
+
return ids;
|
|
733
|
+
}, {}),
|
|
734
|
+
);
|
|
735
|
+
|
|
736
|
+
const buffer = pointConnections.getData().opacities;
|
|
737
|
+
|
|
738
|
+
const unselectedLineIds = lineIds.filter(
|
|
739
|
+
(lineId) => !selectedPointsConnectionSet.has(+lineId),
|
|
740
|
+
);
|
|
741
|
+
|
|
742
|
+
for (const lineId of unselectedLineIds) {
|
|
743
|
+
const index = pointConnectionMap[lineId][0];
|
|
744
|
+
const numPointPerLine = pointConnectionMap[lineId][2];
|
|
745
|
+
const pointOffset = pointConnectionMap[lineId][3];
|
|
746
|
+
|
|
747
|
+
const bufferStart = index * 4 + pointOffset * 2;
|
|
748
|
+
const bufferEnd = bufferStart + numPointPerLine * 2 + 4;
|
|
749
|
+
|
|
750
|
+
if (buffer.__original__ === undefined) {
|
|
751
|
+
buffer.__original__ = buffer.slice();
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
for (let i = bufferStart; i < bufferEnd; i++) {
|
|
755
|
+
// buffer[i] = Math.floor(buffer[i] / 4) * 4 + stateIndex;
|
|
756
|
+
buffer[i] = isNormal
|
|
757
|
+
? buffer.__original__[i]
|
|
758
|
+
: pointConnectionOpacityActive;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
lineIdCacher(lineId);
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
pointConnections.getBuffer().opacities.subdata(buffer, 0);
|
|
765
|
+
};
|
|
766
|
+
|
|
767
|
+
const indexToStateTexCoord = (index) => [
|
|
768
|
+
(index % stateTexRes) / stateTexRes + stateTexEps,
|
|
769
|
+
Math.floor(index / stateTexRes) / stateTexRes + stateTexEps,
|
|
770
|
+
];
|
|
771
|
+
|
|
772
|
+
const isPointsFilteredOut = (pointIdx) =>
|
|
773
|
+
isPointsFiltered && !filteredPointsSet.has(pointIdx);
|
|
774
|
+
|
|
775
|
+
const deselect = ({ preventEvent = false } = {}) => {
|
|
776
|
+
if (lassoClearEvent === LASSO_CLEAR_ON_DESELECT) {
|
|
777
|
+
lassoClear();
|
|
778
|
+
}
|
|
779
|
+
if (selectedPoints.length > 0) {
|
|
780
|
+
if (!preventEvent) {
|
|
781
|
+
pubSub.publish('deselect');
|
|
782
|
+
}
|
|
783
|
+
selectedPointsConnectionSet.clear();
|
|
784
|
+
setPointConnectionColorState(selectedPoints, 0);
|
|
785
|
+
selectedPoints = [];
|
|
786
|
+
selectedPointsSet.clear();
|
|
787
|
+
draw = true;
|
|
788
|
+
}
|
|
789
|
+
};
|
|
790
|
+
|
|
791
|
+
/**
|
|
792
|
+
* Select and highlight a set of points
|
|
793
|
+
* @param {number | number[]} pointIdxs
|
|
794
|
+
* @param {import('./types').ScatterplotMethodOptions['select']}
|
|
795
|
+
*/
|
|
796
|
+
const select = (
|
|
797
|
+
pointIdxs,
|
|
798
|
+
{
|
|
799
|
+
intersect = false,
|
|
800
|
+
merge = false,
|
|
801
|
+
remove = false,
|
|
802
|
+
preventEvent = false,
|
|
803
|
+
} = {},
|
|
804
|
+
) => {
|
|
805
|
+
const newSelectedPoints = Array.isArray(pointIdxs)
|
|
806
|
+
? pointIdxs
|
|
807
|
+
: [pointIdxs];
|
|
808
|
+
const currSelectedPoints = [...selectedPoints];
|
|
809
|
+
|
|
810
|
+
if (intersect) {
|
|
811
|
+
const newSelectedPointsSet = new Set(newSelectedPoints);
|
|
812
|
+
|
|
813
|
+
selectedPoints =
|
|
814
|
+
selectedPoints.length > 0
|
|
815
|
+
? selectedPoints.filter((point) => newSelectedPointsSet.has(point))
|
|
816
|
+
: newSelectedPoints;
|
|
817
|
+
|
|
818
|
+
if (currSelectedPoints.length === selectedPoints.length) {
|
|
819
|
+
draw = true;
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
} else if (merge) {
|
|
823
|
+
selectedPoints = unionIntegers(selectedPoints, newSelectedPoints);
|
|
824
|
+
if (currSelectedPoints.length === selectedPoints.length) {
|
|
825
|
+
draw = true;
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
} else if (remove) {
|
|
829
|
+
const newSelectedPointsSet = new Set(newSelectedPoints);
|
|
830
|
+
selectedPoints = selectedPoints.filter(
|
|
831
|
+
(point) => !newSelectedPointsSet.has(point),
|
|
832
|
+
);
|
|
833
|
+
if (currSelectedPoints.length === selectedPoints.length) {
|
|
834
|
+
draw = true;
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
} else {
|
|
838
|
+
// Unset previously highlight point connections
|
|
839
|
+
if (selectedPoints?.length > 0) {
|
|
840
|
+
setPointConnectionColorState(selectedPoints, 0);
|
|
841
|
+
}
|
|
842
|
+
if (currSelectedPoints.length > 0 && newSelectedPoints.length === 0) {
|
|
843
|
+
deselect({ preventEvent });
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
selectedPoints = newSelectedPoints;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
if (hasSameElements(currSelectedPoints, selectedPoints)) {
|
|
850
|
+
draw = true;
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
const selectedPointsBuffer = [];
|
|
855
|
+
|
|
856
|
+
selectedPointsSet.clear();
|
|
857
|
+
selectedPointsConnectionSet.clear();
|
|
858
|
+
|
|
859
|
+
for (let i = selectedPoints.length - 1; i >= 0; i--) {
|
|
860
|
+
const pointIdx = selectedPoints[i];
|
|
861
|
+
|
|
862
|
+
if (
|
|
863
|
+
pointIdx < 0 ||
|
|
864
|
+
pointIdx >= numPoints ||
|
|
865
|
+
isPointsFilteredOut(pointIdx)
|
|
866
|
+
) {
|
|
867
|
+
// Remove invalid selected points
|
|
868
|
+
selectedPoints.splice(i, 1);
|
|
869
|
+
continue;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
selectedPointsSet.add(pointIdx);
|
|
873
|
+
selectedPointsBuffer.push.apply(
|
|
874
|
+
selectedPointsBuffer,
|
|
875
|
+
indexToStateTexCoord(pointIdx),
|
|
876
|
+
);
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
selectedPointsIndexBuffer({
|
|
880
|
+
usage: 'dynamic',
|
|
881
|
+
type: 'float',
|
|
882
|
+
data: selectedPointsBuffer,
|
|
883
|
+
});
|
|
884
|
+
|
|
885
|
+
setPointConnectionColorState(selectedPoints, 1);
|
|
886
|
+
|
|
887
|
+
if (!preventEvent) {
|
|
888
|
+
pubSub.publish('select', { points: selectedPoints });
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
draw = true;
|
|
892
|
+
};
|
|
893
|
+
|
|
894
|
+
/**
|
|
895
|
+
* @param {number} point
|
|
896
|
+
* @param {import('./types').ScatterplotMethodOptions['hover']} options
|
|
897
|
+
*/
|
|
898
|
+
const hover = (
|
|
899
|
+
point,
|
|
900
|
+
{ showReticleOnce = false, preventEvent = false } = {},
|
|
901
|
+
) => {
|
|
902
|
+
let needsRedraw = false;
|
|
903
|
+
|
|
904
|
+
const isFilteredOut = isPointsFiltered && !filteredPointsSet.has(point);
|
|
905
|
+
|
|
906
|
+
if (!isFilteredOut && point >= 0 && point < numPoints) {
|
|
907
|
+
needsRedraw = true;
|
|
908
|
+
const oldHoveredPoint = hoveredPoint;
|
|
909
|
+
const newHoveredPoint = point !== hoveredPoint;
|
|
910
|
+
if (
|
|
911
|
+
+oldHoveredPoint >= 0 &&
|
|
912
|
+
newHoveredPoint &&
|
|
913
|
+
!selectedPointsSet.has(oldHoveredPoint)
|
|
914
|
+
) {
|
|
915
|
+
setPointConnectionColorState([oldHoveredPoint], 0);
|
|
916
|
+
}
|
|
917
|
+
hoveredPoint = point;
|
|
918
|
+
hoveredPointIndexBuffer.subdata(indexToStateTexCoord(point));
|
|
919
|
+
if (!selectedPointsSet.has(point)) {
|
|
920
|
+
setPointConnectionColorState([point], 2);
|
|
921
|
+
}
|
|
922
|
+
if (newHoveredPoint && !preventEvent) {
|
|
923
|
+
pubSub.publish('pointover', hoveredPoint);
|
|
924
|
+
}
|
|
925
|
+
} else {
|
|
926
|
+
needsRedraw = +hoveredPoint >= 0;
|
|
927
|
+
if (needsRedraw) {
|
|
928
|
+
if (!selectedPointsSet.has(hoveredPoint)) {
|
|
929
|
+
setPointConnectionColorState([hoveredPoint], 0);
|
|
930
|
+
}
|
|
931
|
+
if (!preventEvent) {
|
|
932
|
+
pubSub.publish('pointout', hoveredPoint);
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
hoveredPoint = undefined;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
if (needsRedraw) {
|
|
939
|
+
draw = true;
|
|
940
|
+
drawReticleOnce = showReticleOnce;
|
|
941
|
+
}
|
|
942
|
+
};
|
|
943
|
+
|
|
944
|
+
const getRelativeMousePosition = (event) => {
|
|
945
|
+
const rect = canvas.getBoundingClientRect();
|
|
946
|
+
|
|
947
|
+
mousePosition[0] = event.clientX - rect.left;
|
|
948
|
+
mousePosition[1] = event.clientY - rect.top;
|
|
949
|
+
|
|
950
|
+
return [...mousePosition];
|
|
951
|
+
};
|
|
952
|
+
|
|
953
|
+
const lassoStart = () => {
|
|
954
|
+
// Fix camera for the lasso selection
|
|
955
|
+
camera.config({ isFixed: true });
|
|
956
|
+
mouseDown = true;
|
|
957
|
+
lassoActive = true;
|
|
958
|
+
lassoClear();
|
|
959
|
+
if (mouseDownTimeout >= 0) {
|
|
960
|
+
clearTimeout(mouseDownTimeout);
|
|
961
|
+
mouseDownTimeout = -1;
|
|
962
|
+
}
|
|
963
|
+
pubSub.publish('lassoStart');
|
|
964
|
+
};
|
|
965
|
+
|
|
966
|
+
const lassoEnd = (
|
|
967
|
+
lassoPoints,
|
|
968
|
+
lassoPointsFlat,
|
|
969
|
+
{ intersect = false, merge = false, remove = false } = {},
|
|
970
|
+
) => {
|
|
971
|
+
camera.config({ isFixed: cameraIsFixed });
|
|
972
|
+
lassoPointsCurr = [...lassoPoints];
|
|
973
|
+
const pointsInLasso = findPointsInLasso(lassoPointsFlat);
|
|
974
|
+
select(pointsInLasso, { intersect, merge, remove });
|
|
975
|
+
|
|
976
|
+
pubSub.publish('lassoEnd', {
|
|
977
|
+
coordinates: lassoPointsCurr,
|
|
978
|
+
});
|
|
979
|
+
if (lassoClearEvent === LASSO_CLEAR_ON_END) {
|
|
980
|
+
lassoClear();
|
|
981
|
+
}
|
|
982
|
+
};
|
|
983
|
+
|
|
984
|
+
const lassoManager = createLassoManager(canvas, {
|
|
985
|
+
onStart: lassoStart,
|
|
986
|
+
onDraw: lassoExtend,
|
|
987
|
+
onEnd: lassoEnd,
|
|
988
|
+
enableInitiator: lassoInitiator,
|
|
989
|
+
initiatorParentElement: lassoInitiatorParentElement,
|
|
990
|
+
longPressIndicatorParentElement: lassoLongPressIndicatorParentElement,
|
|
991
|
+
pointNorm: ([x, y]) => getScatterGlPos(getNdcX(x), getNdcY(y)),
|
|
992
|
+
minDelay: lassoMinDelay,
|
|
993
|
+
minDist:
|
|
994
|
+
lassoType === 'brush'
|
|
995
|
+
? Math.max(LASSO_BRUSH_MIN_MIN_DIST, lassoMinDist)
|
|
996
|
+
: lassoMinDist,
|
|
997
|
+
type: lassoType,
|
|
998
|
+
});
|
|
999
|
+
|
|
1000
|
+
const checkLassoMode = () => mouseMode === MOUSE_MODE_LASSO;
|
|
1001
|
+
|
|
1002
|
+
const checkModKey = (event, action) => {
|
|
1003
|
+
switch (actionKeyMap[action]) {
|
|
1004
|
+
case KEY_ALT:
|
|
1005
|
+
return event.altKey;
|
|
1006
|
+
|
|
1007
|
+
case KEY_CMD:
|
|
1008
|
+
return event.metaKey;
|
|
1009
|
+
|
|
1010
|
+
case KEY_CTRL:
|
|
1011
|
+
return event.ctrlKey;
|
|
1012
|
+
|
|
1013
|
+
case KEY_META:
|
|
1014
|
+
return event.metaKey;
|
|
1015
|
+
|
|
1016
|
+
case KEY_SHIFT:
|
|
1017
|
+
return event.shiftKey;
|
|
1018
|
+
|
|
1019
|
+
default:
|
|
1020
|
+
return false;
|
|
1021
|
+
}
|
|
1022
|
+
};
|
|
1023
|
+
|
|
1024
|
+
const checkIfMouseIsOverCanvas = (event) =>
|
|
1025
|
+
document
|
|
1026
|
+
.elementsFromPoint(event.clientX, event.clientY)
|
|
1027
|
+
.some((element) => element === canvas);
|
|
1028
|
+
|
|
1029
|
+
const mouseDownHandler = (event) => {
|
|
1030
|
+
if (!isPointsDrawn || event.buttons !== 1) {
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
mouseDown = true;
|
|
1035
|
+
mouseDownTime = performance.now();
|
|
1036
|
+
|
|
1037
|
+
mouseDownPosition = getRelativeMousePosition(event);
|
|
1038
|
+
lassoActive = checkLassoMode() || checkModKey(event, KEY_ACTION_LASSO);
|
|
1039
|
+
|
|
1040
|
+
if (!lassoActive && lassoOnLongPress) {
|
|
1041
|
+
lassoManager.showLongPressIndicator(event.clientX, event.clientY, {
|
|
1042
|
+
time: lassoLongPressTime,
|
|
1043
|
+
extraTime: lassoLongPressAfterEffectTime,
|
|
1044
|
+
delay: lassoLongPressEffectDelay,
|
|
1045
|
+
});
|
|
1046
|
+
mouseDownTimeout = setTimeout(() => {
|
|
1047
|
+
mouseDownTimeout = -1;
|
|
1048
|
+
lassoActive = true;
|
|
1049
|
+
}, lassoLongPressTime);
|
|
1050
|
+
}
|
|
1051
|
+
};
|
|
1052
|
+
|
|
1053
|
+
const mouseUpHandler = (event) => {
|
|
1054
|
+
if (!isPointsDrawn) {
|
|
1055
|
+
return;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
mouseDown = false;
|
|
1059
|
+
if (mouseDownTimeout >= 0) {
|
|
1060
|
+
clearTimeout(mouseDownTimeout);
|
|
1061
|
+
mouseDownTimeout = -1;
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
if (lassoActive) {
|
|
1065
|
+
event.preventDefault();
|
|
1066
|
+
lassoActive = false;
|
|
1067
|
+
|
|
1068
|
+
const hasLassoModeCfg = !!lassoMode;
|
|
1069
|
+
|
|
1070
|
+
lassoManager.end({
|
|
1071
|
+
intersect: hasLassoModeCfg
|
|
1072
|
+
? lassoMode === 'intersect'
|
|
1073
|
+
: checkModKey(event, KEY_ACTION_INTERSECT),
|
|
1074
|
+
merge: hasLassoModeCfg
|
|
1075
|
+
? lassoMode === 'merge'
|
|
1076
|
+
: checkModKey(event, KEY_ACTION_MERGE),
|
|
1077
|
+
remove: hasLassoModeCfg
|
|
1078
|
+
? lassoMode === 'remove'
|
|
1079
|
+
: checkModKey(event, KEY_ACTION_REMOVE),
|
|
1080
|
+
});
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
if (lassoOnLongPress) {
|
|
1084
|
+
lassoManager.hideLongPressIndicator({
|
|
1085
|
+
time: lassoLongPressRevertEffectTime,
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1088
|
+
};
|
|
1089
|
+
|
|
1090
|
+
const mouseClickHandler = (event) => {
|
|
1091
|
+
if (!isPointsDrawn) {
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
event.preventDefault();
|
|
1096
|
+
|
|
1097
|
+
const currentMousePosition = getRelativeMousePosition(event);
|
|
1098
|
+
|
|
1099
|
+
if (dist(...currentMousePosition, ...mouseDownPosition) >= lassoMinDist) {
|
|
1100
|
+
return;
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
const clickTime = performance.now() - mouseDownTime;
|
|
1104
|
+
|
|
1105
|
+
if (!lassoInitiator || clickTime < LONG_CLICK_TIME) {
|
|
1106
|
+
// If the user clicked normally (i.e., fast) we'll only show the lasso
|
|
1107
|
+
// initiator if the use click into the void
|
|
1108
|
+
const clostestPoint = raycast();
|
|
1109
|
+
if (clostestPoint >= 0) {
|
|
1110
|
+
if (
|
|
1111
|
+
selectedPoints.length > 0 &&
|
|
1112
|
+
lassoClearEvent === LASSO_CLEAR_ON_DESELECT
|
|
1113
|
+
) {
|
|
1114
|
+
// Special case where we silently "deselect" the previous points by
|
|
1115
|
+
// overriding the selected points. Hence, we need to clear the lasso.
|
|
1116
|
+
lassoClear();
|
|
1117
|
+
}
|
|
1118
|
+
select([clostestPoint], {
|
|
1119
|
+
merge: checkModKey(event, KEY_ACTION_MERGE),
|
|
1120
|
+
remove: checkModKey(event, KEY_ACTION_REMOVE),
|
|
1121
|
+
});
|
|
1122
|
+
} else if (!lassoInitiatorTimeout) {
|
|
1123
|
+
// We'll also wait to make sure the user didn't double click
|
|
1124
|
+
lassoInitiatorTimeout = setTimeout(() => {
|
|
1125
|
+
lassoInitiatorTimeout = null;
|
|
1126
|
+
lassoManager.showInitiator(event);
|
|
1127
|
+
}, SINGLE_CLICK_DELAY);
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
};
|
|
1131
|
+
|
|
1132
|
+
const mouseDblClickHandler = (event) => {
|
|
1133
|
+
lassoManager.hideInitiator();
|
|
1134
|
+
if (lassoInitiatorTimeout) {
|
|
1135
|
+
clearTimeout(lassoInitiatorTimeout);
|
|
1136
|
+
lassoInitiatorTimeout = null;
|
|
1137
|
+
}
|
|
1138
|
+
if (deselectOnDblClick) {
|
|
1139
|
+
event.preventDefault();
|
|
1140
|
+
deselect();
|
|
1141
|
+
}
|
|
1142
|
+
};
|
|
1143
|
+
|
|
1144
|
+
const mouseMoveHandler = (event) => {
|
|
1145
|
+
if (!isMouseOverCanvasChecked) {
|
|
1146
|
+
isMouseInCanvas = checkIfMouseIsOverCanvas(event);
|
|
1147
|
+
isMouseOverCanvasChecked = true;
|
|
1148
|
+
}
|
|
1149
|
+
if (!(isPointsDrawn && (isMouseInCanvas || mouseDown))) {
|
|
1150
|
+
return;
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
const currentMousePosition = getRelativeMousePosition(event);
|
|
1154
|
+
const mouseMoveDist = dist(...currentMousePosition, ...mouseDownPosition);
|
|
1155
|
+
const mouseMovedMin = mouseMoveDist >= lassoMinDist;
|
|
1156
|
+
|
|
1157
|
+
// Only ray cast if the mouse cursor is inside
|
|
1158
|
+
if (isMouseInCanvas && !lassoActive) {
|
|
1159
|
+
hover(raycast()); // eslint-disable-line no-use-before-define
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
if (lassoActive) {
|
|
1163
|
+
event.preventDefault();
|
|
1164
|
+
lassoManager.extend(event, true);
|
|
1165
|
+
} else if (mouseDown && lassoOnLongPress && mouseMovedMin) {
|
|
1166
|
+
lassoManager.hideLongPressIndicator({
|
|
1167
|
+
time: lassoLongPressRevertEffectTime,
|
|
1168
|
+
});
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
if (mouseDownTimeout >= 0 && mouseMovedMin) {
|
|
1172
|
+
clearTimeout(mouseDownTimeout);
|
|
1173
|
+
mouseDownTimeout = -1;
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
// Always redraw when mousedown as the user might have panned or lassoed
|
|
1177
|
+
if (mouseDown) {
|
|
1178
|
+
draw = true;
|
|
1179
|
+
}
|
|
1180
|
+
};
|
|
1181
|
+
|
|
1182
|
+
const blurHandler = () => {
|
|
1183
|
+
hoveredPoint = undefined;
|
|
1184
|
+
isMouseInCanvas = false;
|
|
1185
|
+
isMouseOverCanvasChecked = false;
|
|
1186
|
+
|
|
1187
|
+
if (!isPointsDrawn) {
|
|
1188
|
+
return;
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
if (+hoveredPoint >= 0 && !selectedPointsSet.has(hoveredPoint)) {
|
|
1192
|
+
setPointConnectionColorState([hoveredPoint], 0);
|
|
1193
|
+
}
|
|
1194
|
+
mouseUpHandler();
|
|
1195
|
+
draw = true;
|
|
1196
|
+
};
|
|
1197
|
+
|
|
1198
|
+
const createEncodingTexture = () => {
|
|
1199
|
+
const maxEncoding = Math.max(pointSize.length, opacity.length);
|
|
1200
|
+
|
|
1201
|
+
encodingTexRes = Math.max(2, Math.ceil(Math.sqrt(maxEncoding)));
|
|
1202
|
+
const rgba = new Float32Array(encodingTexRes ** 2 * 4);
|
|
1203
|
+
|
|
1204
|
+
for (let i = 0; i < maxEncoding; i++) {
|
|
1205
|
+
rgba[i * 4] = pointSize[i] || 0;
|
|
1206
|
+
rgba[i * 4 + 1] = Math.min(1, opacity[i] || 0);
|
|
1207
|
+
|
|
1208
|
+
const activeOpacity = Number(
|
|
1209
|
+
(pointColorActive[i] || pointColorActive[0])[3],
|
|
1210
|
+
);
|
|
1211
|
+
rgba[i * 4 + 2] = Math.min(
|
|
1212
|
+
1,
|
|
1213
|
+
Number.isNaN(activeOpacity) ? 1 : activeOpacity,
|
|
1214
|
+
);
|
|
1215
|
+
|
|
1216
|
+
const hoverOpacity = Number(
|
|
1217
|
+
(pointColorHover[i] || pointColorHover[0])[3],
|
|
1218
|
+
);
|
|
1219
|
+
rgba[i * 4 + 3] = Math.min(
|
|
1220
|
+
1,
|
|
1221
|
+
Number.isNaN(hoverOpacity) ? 1 : hoverOpacity,
|
|
1222
|
+
);
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
return renderer.regl.texture({
|
|
1226
|
+
data: rgba,
|
|
1227
|
+
shape: [encodingTexRes, encodingTexRes, 4],
|
|
1228
|
+
type: 'float',
|
|
1229
|
+
});
|
|
1230
|
+
};
|
|
1231
|
+
|
|
1232
|
+
const getColors = (
|
|
1233
|
+
baseColor = pointColor,
|
|
1234
|
+
activeColor = pointColorActive,
|
|
1235
|
+
hoverColor = pointColorHover,
|
|
1236
|
+
) => {
|
|
1237
|
+
const n = baseColor.length;
|
|
1238
|
+
const n2 = activeColor.length;
|
|
1239
|
+
const n3 = hoverColor.length;
|
|
1240
|
+
const colors = [];
|
|
1241
|
+
if (n === n2 && n2 === n3) {
|
|
1242
|
+
for (let i = 0; i < n; i++) {
|
|
1243
|
+
colors.push(
|
|
1244
|
+
baseColor[i],
|
|
1245
|
+
activeColor[i],
|
|
1246
|
+
hoverColor[i],
|
|
1247
|
+
backgroundColor,
|
|
1248
|
+
);
|
|
1249
|
+
}
|
|
1250
|
+
} else {
|
|
1251
|
+
for (let i = 0; i < n; i++) {
|
|
1252
|
+
const rgbaOpaque = [
|
|
1253
|
+
baseColor[i][0],
|
|
1254
|
+
baseColor[i][1],
|
|
1255
|
+
baseColor[i][2],
|
|
1256
|
+
1,
|
|
1257
|
+
];
|
|
1258
|
+
const colorActive =
|
|
1259
|
+
colorBy === DEFAULT_COLOR_BY ? activeColor[0] : rgbaOpaque;
|
|
1260
|
+
const colorHover =
|
|
1261
|
+
colorBy === DEFAULT_COLOR_BY ? hoverColor[0] : rgbaOpaque;
|
|
1262
|
+
colors.push(baseColor[i], colorActive, colorHover, backgroundColor);
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
return colors;
|
|
1266
|
+
};
|
|
1267
|
+
|
|
1268
|
+
const createColorTexture = () => {
|
|
1269
|
+
const colors = getColors();
|
|
1270
|
+
const numColors = colors.length;
|
|
1271
|
+
colorTexRes = Math.max(2, Math.ceil(Math.sqrt(numColors)));
|
|
1272
|
+
const rgba = new Float32Array(colorTexRes ** 2 * 4);
|
|
1273
|
+
colors.forEach((color, i) => {
|
|
1274
|
+
rgba[i * 4] = color[0]; // r
|
|
1275
|
+
rgba[i * 4 + 1] = color[1]; // g
|
|
1276
|
+
rgba[i * 4 + 2] = color[2]; // b
|
|
1277
|
+
rgba[i * 4 + 3] = color[3]; // a
|
|
1278
|
+
});
|
|
1279
|
+
|
|
1280
|
+
return renderer.regl.texture({
|
|
1281
|
+
data: rgba,
|
|
1282
|
+
shape: [colorTexRes, colorTexRes, 4],
|
|
1283
|
+
type: 'float',
|
|
1284
|
+
});
|
|
1285
|
+
};
|
|
1286
|
+
|
|
1287
|
+
/**
|
|
1288
|
+
* Since we're using an external renderer whose canvas' width and height
|
|
1289
|
+
* might differ from this instance's width and height, we have to adjust the
|
|
1290
|
+
* projection of camera spaces into clip space accordingly.
|
|
1291
|
+
*
|
|
1292
|
+
* The `widthRatio` is rendererCanvas.width / thisCanvas.width
|
|
1293
|
+
* The `heightRatio` is rendererCanvas.height / thisCanvas.height
|
|
1294
|
+
*/
|
|
1295
|
+
const updateProjectionMatrix = (widthRatio, heightRatio) => {
|
|
1296
|
+
projection[0] = widthRatio / viewAspectRatio;
|
|
1297
|
+
projection[5] = heightRatio;
|
|
1298
|
+
};
|
|
1299
|
+
|
|
1300
|
+
const updateViewAspectRatio = () => {
|
|
1301
|
+
viewAspectRatio = currentWidth / currentHeight;
|
|
1302
|
+
projectionLocal = mat4.fromScaling([], [1 / viewAspectRatio, 1, 1]);
|
|
1303
|
+
projection = mat4.fromScaling([], [1 / viewAspectRatio, 1, 1]);
|
|
1304
|
+
model = mat4.fromScaling([], [dataAspectRatio, 1, 1]);
|
|
1305
|
+
};
|
|
1306
|
+
|
|
1307
|
+
const setDataAspectRatio = (newDataAspectRatio) => {
|
|
1308
|
+
if (+newDataAspectRatio <= 0) {
|
|
1309
|
+
return;
|
|
1310
|
+
}
|
|
1311
|
+
dataAspectRatio = newDataAspectRatio;
|
|
1312
|
+
};
|
|
1313
|
+
|
|
1314
|
+
const setColors = (getter, setter) => (newColors) => {
|
|
1315
|
+
if (!newColors || newColors.length === 0) {
|
|
1316
|
+
return;
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
const colors = getter();
|
|
1320
|
+
const prevColors = [...colors];
|
|
1321
|
+
|
|
1322
|
+
let tmpColors = isMultipleColors(newColors) ? newColors : [newColors];
|
|
1323
|
+
tmpColors = tmpColors.map((color) => toRgba(color, true));
|
|
1324
|
+
|
|
1325
|
+
if (isSameRgbas(prevColors, tmpColors)) {
|
|
1326
|
+
// We don't need to update the color texture so we return early
|
|
1327
|
+
return;
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
if (colorTex) {
|
|
1331
|
+
colorTex.destroy();
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
try {
|
|
1335
|
+
setter(tmpColors);
|
|
1336
|
+
colorTex = createColorTexture();
|
|
1337
|
+
} catch (_error) {
|
|
1338
|
+
// biome-ignore lint/suspicious/noConsole: This is a legitimately useful warning
|
|
1339
|
+
console.error('Invalid colors. Switching back to default colors.');
|
|
1340
|
+
setter(prevColors);
|
|
1341
|
+
colorTex = createColorTexture();
|
|
1342
|
+
}
|
|
1343
|
+
};
|
|
1344
|
+
|
|
1345
|
+
const setPointColor = setColors(
|
|
1346
|
+
() => pointColor,
|
|
1347
|
+
(colors) => {
|
|
1348
|
+
pointColor = colors;
|
|
1349
|
+
},
|
|
1350
|
+
);
|
|
1351
|
+
const setPointColorActive = setColors(
|
|
1352
|
+
() => pointColorActive,
|
|
1353
|
+
(colors) => {
|
|
1354
|
+
pointColorActive = colors;
|
|
1355
|
+
},
|
|
1356
|
+
);
|
|
1357
|
+
const setPointColorHover = setColors(
|
|
1358
|
+
() => pointColorHover,
|
|
1359
|
+
(colors) => {
|
|
1360
|
+
pointColorHover = colors;
|
|
1361
|
+
},
|
|
1362
|
+
);
|
|
1363
|
+
|
|
1364
|
+
const computeDomainView = () => {
|
|
1365
|
+
const xyStartPt = getScatterGlPos(-1, -1);
|
|
1366
|
+
const xyEndPt = getScatterGlPos(1, 1);
|
|
1367
|
+
|
|
1368
|
+
const xStart = (xyStartPt[0] + 1) / 2;
|
|
1369
|
+
const xEnd = (xyEndPt[0] + 1) / 2;
|
|
1370
|
+
const yStart = (xyStartPt[1] + 1) / 2;
|
|
1371
|
+
const yEnd = (xyEndPt[1] + 1) / 2;
|
|
1372
|
+
|
|
1373
|
+
const xDomainView = [
|
|
1374
|
+
xDomainStart + xStart * xDomainSize,
|
|
1375
|
+
xDomainStart + xEnd * xDomainSize,
|
|
1376
|
+
];
|
|
1377
|
+
const yDomainView = [
|
|
1378
|
+
yDomainStart + yStart * yDomainSize,
|
|
1379
|
+
yDomainStart + yEnd * yDomainSize,
|
|
1380
|
+
];
|
|
1381
|
+
|
|
1382
|
+
return [xDomainView, yDomainView];
|
|
1383
|
+
};
|
|
1384
|
+
|
|
1385
|
+
const updateScales = () => {
|
|
1386
|
+
if (!(xScale || yScale)) {
|
|
1387
|
+
return;
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
const [xDomainView, yDomainView] = computeDomainView();
|
|
1391
|
+
|
|
1392
|
+
if (xScale) {
|
|
1393
|
+
xScale.domain(xDomainView);
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
if (yScale) {
|
|
1397
|
+
yScale.domain(yDomainView);
|
|
1398
|
+
}
|
|
1399
|
+
};
|
|
1400
|
+
|
|
1401
|
+
const setCurrentHeight = (newCurrentHeight, skipScaleUpdates) => {
|
|
1402
|
+
currentHeight = Math.max(1, newCurrentHeight);
|
|
1403
|
+
canvas.height = Math.floor(currentHeight * window.devicePixelRatio);
|
|
1404
|
+
if (yScale) {
|
|
1405
|
+
yScale.range([currentHeight, 0]);
|
|
1406
|
+
if (!skipScaleUpdates) {
|
|
1407
|
+
updateScales();
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
};
|
|
1411
|
+
|
|
1412
|
+
const setHeight = (newHeight) => {
|
|
1413
|
+
if (newHeight === AUTO) {
|
|
1414
|
+
height = newHeight;
|
|
1415
|
+
canvas.style.height = '100%';
|
|
1416
|
+
window.requestAnimationFrame(() => {
|
|
1417
|
+
if (canvas) {
|
|
1418
|
+
setCurrentHeight(canvas.getBoundingClientRect().height);
|
|
1419
|
+
}
|
|
1420
|
+
});
|
|
1421
|
+
return;
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
if (!+newHeight || +newHeight <= 0) {
|
|
1425
|
+
return;
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
height = +newHeight;
|
|
1429
|
+
setCurrentHeight(height);
|
|
1430
|
+
canvas.style.height = `${height}px`;
|
|
1431
|
+
};
|
|
1432
|
+
|
|
1433
|
+
const computePointSizeMouseDetection = () => {
|
|
1434
|
+
computedPointSizeMouseDetection = pointSizeMouseDetection;
|
|
1435
|
+
|
|
1436
|
+
if (pointSizeMouseDetection === AUTO) {
|
|
1437
|
+
computedPointSizeMouseDetection = Array.isArray(pointSize)
|
|
1438
|
+
? maxArray(pointSize)
|
|
1439
|
+
: pointSize;
|
|
1440
|
+
}
|
|
1441
|
+
};
|
|
1442
|
+
|
|
1443
|
+
const setPointSize = (newPointSize) => {
|
|
1444
|
+
const oldPointSize = Array.isArray(pointSize) ? [...pointSize] : pointSize;
|
|
1445
|
+
|
|
1446
|
+
if (isConditionalArray(newPointSize, isPositiveNumber, { minLength: 1 })) {
|
|
1447
|
+
pointSize = [...newPointSize];
|
|
1448
|
+
} else if (isStrictlyPositiveNumber(+newPointSize)) {
|
|
1449
|
+
pointSize = [+newPointSize];
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
if (
|
|
1453
|
+
oldPointSize === pointSize ||
|
|
1454
|
+
hasSameElements(oldPointSize, pointSize)
|
|
1455
|
+
) {
|
|
1456
|
+
// We don't need to update the encoding texture so we return early
|
|
1457
|
+
return;
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
if (encodingTex) {
|
|
1461
|
+
encodingTex.destroy();
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
minPointScale = MIN_POINT_SIZE / pointSize[0];
|
|
1465
|
+
encodingTex = createEncodingTexture();
|
|
1466
|
+
computePointSizeMouseDetection();
|
|
1467
|
+
};
|
|
1468
|
+
|
|
1469
|
+
const setPointSizeSelected = (newPointSizeSelected) => {
|
|
1470
|
+
if (!+newPointSizeSelected || +newPointSizeSelected < 0) {
|
|
1471
|
+
return;
|
|
1472
|
+
}
|
|
1473
|
+
pointSizeSelected = +newPointSizeSelected;
|
|
1474
|
+
};
|
|
1475
|
+
|
|
1476
|
+
const setPointOutlineWidth = (newPointOutlineWidth) => {
|
|
1477
|
+
if (!+newPointOutlineWidth || +newPointOutlineWidth < 0) {
|
|
1478
|
+
return;
|
|
1479
|
+
}
|
|
1480
|
+
pointOutlineWidth = +newPointOutlineWidth;
|
|
1481
|
+
};
|
|
1482
|
+
|
|
1483
|
+
const setCurrentWidth = (newCurrentWidth, skipScaleUpdates) => {
|
|
1484
|
+
currentWidth = Math.max(1, newCurrentWidth);
|
|
1485
|
+
canvas.width = Math.floor(currentWidth * window.devicePixelRatio);
|
|
1486
|
+
if (xScale) {
|
|
1487
|
+
xScale.range([0, currentWidth]);
|
|
1488
|
+
if (!skipScaleUpdates) {
|
|
1489
|
+
updateScales();
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
};
|
|
1493
|
+
|
|
1494
|
+
const setWidth = (newWidth) => {
|
|
1495
|
+
if (newWidth === AUTO) {
|
|
1496
|
+
width = newWidth;
|
|
1497
|
+
canvas.style.width = '100%';
|
|
1498
|
+
window.requestAnimationFrame(() => {
|
|
1499
|
+
if (canvas) {
|
|
1500
|
+
setCurrentWidth(canvas.getBoundingClientRect().width);
|
|
1501
|
+
}
|
|
1502
|
+
});
|
|
1503
|
+
return;
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
if (!+newWidth || +newWidth <= 0) {
|
|
1507
|
+
return;
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
width = +newWidth;
|
|
1511
|
+
setCurrentWidth(width);
|
|
1512
|
+
canvas.style.width = `${currentWidth}px`;
|
|
1513
|
+
};
|
|
1514
|
+
|
|
1515
|
+
const setOpacity = (newOpacity) => {
|
|
1516
|
+
const oldOpacity = Array.isArray(opacity) ? [...opacity] : opacity;
|
|
1517
|
+
|
|
1518
|
+
if (isConditionalArray(newOpacity, isPositiveNumber, { minLength: 1 })) {
|
|
1519
|
+
opacity = [...newOpacity];
|
|
1520
|
+
} else if (isStrictlyPositiveNumber(+newOpacity)) {
|
|
1521
|
+
opacity = [+newOpacity];
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
if (oldOpacity === opacity || hasSameElements(oldOpacity, opacity)) {
|
|
1525
|
+
// We don't need to update the encoding texture so we return early
|
|
1526
|
+
return;
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
if (encodingTex) {
|
|
1530
|
+
encodingTex.destroy();
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
encodingTex = createEncodingTexture();
|
|
1534
|
+
};
|
|
1535
|
+
|
|
1536
|
+
const getEncodingDataType = (type) => {
|
|
1537
|
+
switch (type) {
|
|
1538
|
+
case 'valueZ':
|
|
1539
|
+
return valueZDataType;
|
|
1540
|
+
|
|
1541
|
+
case 'valueW':
|
|
1542
|
+
return valueWDataType;
|
|
1543
|
+
|
|
1544
|
+
default:
|
|
1545
|
+
return null;
|
|
1546
|
+
}
|
|
1547
|
+
};
|
|
1548
|
+
|
|
1549
|
+
const getEncodingValueToIdx = (type, rangeValues) => {
|
|
1550
|
+
switch (type) {
|
|
1551
|
+
case CONTINUOUS:
|
|
1552
|
+
return (value) => Math.round(value * (rangeValues.length - 1));
|
|
1553
|
+
|
|
1554
|
+
default:
|
|
1555
|
+
return identity;
|
|
1556
|
+
}
|
|
1557
|
+
};
|
|
1558
|
+
|
|
1559
|
+
const setColorBy = (type) => {
|
|
1560
|
+
colorBy = getEncodingType(type, DEFAULT_COLOR_BY);
|
|
1561
|
+
};
|
|
1562
|
+
const setOpacityBy = (type) => {
|
|
1563
|
+
opacityBy = getEncodingType(type, DEFAULT_OPACITY_BY, {
|
|
1564
|
+
allowDensity: true,
|
|
1565
|
+
});
|
|
1566
|
+
};
|
|
1567
|
+
const setSizeBy = (type) => {
|
|
1568
|
+
sizeBy = getEncodingType(type, DEFAULT_SIZE_BY);
|
|
1569
|
+
};
|
|
1570
|
+
const setPointConnectionColorBy = (type) => {
|
|
1571
|
+
pointConnectionColorBy = getEncodingType(
|
|
1572
|
+
type,
|
|
1573
|
+
DEFAULT_POINT_CONNECTION_COLOR_BY,
|
|
1574
|
+
{ allowSegment: true, allowInherit: true },
|
|
1575
|
+
);
|
|
1576
|
+
};
|
|
1577
|
+
const setPointConnectionOpacityBy = (type) => {
|
|
1578
|
+
pointConnectionOpacityBy = getEncodingType(
|
|
1579
|
+
type,
|
|
1580
|
+
DEFAULT_POINT_CONNECTION_OPACITY_BY,
|
|
1581
|
+
{ allowSegment: true },
|
|
1582
|
+
);
|
|
1583
|
+
};
|
|
1584
|
+
const setPointConnectionSizeBy = (type) => {
|
|
1585
|
+
pointConnectionSizeBy = getEncodingType(
|
|
1586
|
+
type,
|
|
1587
|
+
DEFAULT_POINT_CONNECTION_SIZE_BY,
|
|
1588
|
+
{ allowSegment: true },
|
|
1589
|
+
);
|
|
1590
|
+
};
|
|
1591
|
+
|
|
1592
|
+
const getAntiAliasing = () => antiAliasing;
|
|
1593
|
+
const getResolution = () => [canvas.width, canvas.height];
|
|
1594
|
+
const getBackgroundImage = () => backgroundImage;
|
|
1595
|
+
const getColorTex = () => colorTex;
|
|
1596
|
+
const getColorTexRes = () => colorTexRes;
|
|
1597
|
+
const getColorTexEps = () => 0.5 / colorTexRes;
|
|
1598
|
+
const getDevicePixelRatio = () => window.devicePixelRatio;
|
|
1599
|
+
const getNormalPointsIndexBuffer = () => normalPointsIndexBuffer;
|
|
1600
|
+
const getSelectedPointsIndexBuffer = () => selectedPointsIndexBuffer;
|
|
1601
|
+
const getEncodingTex = () => encodingTex;
|
|
1602
|
+
const getEncodingTexRes = () => encodingTexRes;
|
|
1603
|
+
const getEncodingTexEps = () => 0.5 / encodingTexRes;
|
|
1604
|
+
const getNormalPointSizeExtra = () => 0;
|
|
1605
|
+
const getStateTex = () => tmpStateTex || stateTex;
|
|
1606
|
+
const getStateTexRes = () => stateTexRes;
|
|
1607
|
+
const getStateTexEps = () => 0.5 / stateTexRes;
|
|
1608
|
+
const getProjection = () => projection;
|
|
1609
|
+
const getView = () => camera.view;
|
|
1610
|
+
const getModel = () => model;
|
|
1611
|
+
const getModelViewProjection = () =>
|
|
1612
|
+
mat4.multiply(pvm, projection, mat4.multiply(pvm, camera.view, model));
|
|
1613
|
+
const getConstantPointScale = () => {
|
|
1614
|
+
return window.devicePixelRatio;
|
|
1615
|
+
};
|
|
1616
|
+
const getLinearPointScale = () => {
|
|
1617
|
+
return max(minPointScale, camera.scaling[0]) * window.devicePixelRatio;
|
|
1618
|
+
};
|
|
1619
|
+
const getAsinhPointScale = () => {
|
|
1620
|
+
if (camera.scaling[0] > 1) {
|
|
1621
|
+
return (
|
|
1622
|
+
(Math.asinh(max(1.0, camera.scaling[0])) / Math.asinh(1)) *
|
|
1623
|
+
window.devicePixelRatio
|
|
1624
|
+
);
|
|
1625
|
+
}
|
|
1626
|
+
return max(minPointScale, camera.scaling[0]) * window.devicePixelRatio;
|
|
1627
|
+
};
|
|
1628
|
+
let getPointScale = getAsinhPointScale;
|
|
1629
|
+
if (pointScaleMode === 'linear') {
|
|
1630
|
+
getPointScale = getLinearPointScale;
|
|
1631
|
+
} else if (pointScaleMode === 'constant') {
|
|
1632
|
+
getPointScale = getConstantPointScale;
|
|
1633
|
+
}
|
|
1634
|
+
const getNormalNumPoints = () =>
|
|
1635
|
+
isPointsFiltered ? filteredPointsSet.size : numPoints;
|
|
1636
|
+
const getSelectedNumPoints = () => selectedPoints.length;
|
|
1637
|
+
const getPointOpacityMaxBase = () =>
|
|
1638
|
+
getSelectedNumPoints() > 0 ? opacityInactiveMax : 1;
|
|
1639
|
+
const getPointOpacityScaleBase = () =>
|
|
1640
|
+
getSelectedNumPoints() > 0 ? opacityInactiveScale : 1;
|
|
1641
|
+
const getIsColoredByZ = () => +(colorBy === 'valueZ');
|
|
1642
|
+
const getIsColoredByW = () => +(colorBy === 'valueW');
|
|
1643
|
+
const getIsOpacityByZ = () => +(opacityBy === 'valueZ');
|
|
1644
|
+
const getIsOpacityByW = () => +(opacityBy === 'valueW');
|
|
1645
|
+
const getIsOpacityByDensity = () => +(opacityBy === 'density');
|
|
1646
|
+
const getIsSizedByZ = () => +(sizeBy === 'valueZ');
|
|
1647
|
+
const getIsSizedByW = () => +(sizeBy === 'valueW');
|
|
1648
|
+
const getIsPixelAligned = () => +pixelAligned;
|
|
1649
|
+
const getColorMultiplicator = () => {
|
|
1650
|
+
if (colorBy === 'valueZ') {
|
|
1651
|
+
return valueZDataType === CONTINUOUS ? pointColor.length - 1 : 1;
|
|
1652
|
+
}
|
|
1653
|
+
return valueWDataType === CONTINUOUS ? pointColor.length - 1 : 1;
|
|
1654
|
+
};
|
|
1655
|
+
const getOpacityMultiplicator = () => {
|
|
1656
|
+
if (opacityBy === 'valueZ') {
|
|
1657
|
+
return valueZDataType === CONTINUOUS ? opacity.length - 1 : 1;
|
|
1658
|
+
}
|
|
1659
|
+
return valueWDataType === CONTINUOUS ? opacity.length - 1 : 1;
|
|
1660
|
+
};
|
|
1661
|
+
const getSizeMultiplicator = () => {
|
|
1662
|
+
if (sizeBy === 'valueZ') {
|
|
1663
|
+
return valueZDataType === CONTINUOUS ? pointSize.length - 1 : 1;
|
|
1664
|
+
}
|
|
1665
|
+
return valueWDataType === CONTINUOUS ? pointSize.length - 1 : 1;
|
|
1666
|
+
};
|
|
1667
|
+
const getOpacityDensity = (context) => {
|
|
1668
|
+
if (opacityBy !== 'density') {
|
|
1669
|
+
return 1;
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
// Adopted from the fabulous Ricky Reusser:
|
|
1673
|
+
// https://observablehq.com/@rreusser/selecting-the-right-opacity-for-2d-point-clouds
|
|
1674
|
+
// Extended with a point-density based approach
|
|
1675
|
+
const pointScale = getPointScale();
|
|
1676
|
+
const p = pointSize[0] * pointScale;
|
|
1677
|
+
|
|
1678
|
+
// Compute the plot's x and y range from the view matrix, though these could come from any source
|
|
1679
|
+
const s = (2 / (2 / camera.view[0])) * (2 / (2 / camera.view[5]));
|
|
1680
|
+
|
|
1681
|
+
// Viewport size, in device pixels
|
|
1682
|
+
const H = context.viewportHeight;
|
|
1683
|
+
const W = context.viewportWidth;
|
|
1684
|
+
|
|
1685
|
+
// Adaptation: Instead of using the global number of points, I am using a
|
|
1686
|
+
// density-based approach that takes the points in the view into context
|
|
1687
|
+
// when zooming in. This ensure that in sparse areas, points are opaque and
|
|
1688
|
+
// in dense areas points are more translucent.
|
|
1689
|
+
let alpha =
|
|
1690
|
+
((opacityByDensityFill * W * H) / (numPointsInView * p * p)) * min(1, s);
|
|
1691
|
+
|
|
1692
|
+
// Unless `renderPointsAsSquares` is true, we use circles, which only take
|
|
1693
|
+
// up (pi r^2) of the unit square
|
|
1694
|
+
alpha *= renderPointsAsSquares ? 1 : 1 / (0.25 * Math.PI);
|
|
1695
|
+
|
|
1696
|
+
// If the pixels shrink below the minimum permitted size, then we adjust the opacity instead
|
|
1697
|
+
// and apply clamping of the point size in the vertex shader. Note that we add 0.5 since we
|
|
1698
|
+
// slightly inrease the size of points during rendering to accommodate SDF-style antialiasing.
|
|
1699
|
+
const clampedPointDeviceSize = max(MIN_POINT_SIZE, p) + 0.5;
|
|
1700
|
+
|
|
1701
|
+
// We square this since we're concerned with the ratio of *areas*.
|
|
1702
|
+
alpha *= (p / clampedPointDeviceSize) ** 2;
|
|
1703
|
+
|
|
1704
|
+
// And finally, we clamp to the range [0, 1]. We should really clamp this to 1 / precision
|
|
1705
|
+
// on the low end, depending on the data type of the destination so that we never render *nothing*.
|
|
1706
|
+
return min(1, max(0, alpha));
|
|
1707
|
+
};
|
|
1708
|
+
|
|
1709
|
+
const updatePoints = renderer.regl({
|
|
1710
|
+
framebuffer: () => tmpStateBuffer,
|
|
1711
|
+
|
|
1712
|
+
vert: POINT_UPDATE_VS,
|
|
1713
|
+
frag: POINT_UPDATE_FS,
|
|
1714
|
+
|
|
1715
|
+
attributes: {
|
|
1716
|
+
position: [-4, 0, 4, 4, 4, -4],
|
|
1717
|
+
},
|
|
1718
|
+
|
|
1719
|
+
uniforms: {
|
|
1720
|
+
startStateTex: () => prevStateTex,
|
|
1721
|
+
endStateTex: () => stateTex,
|
|
1722
|
+
t: (_ctx, props) => props.t,
|
|
1723
|
+
},
|
|
1724
|
+
|
|
1725
|
+
count: 3,
|
|
1726
|
+
});
|
|
1727
|
+
|
|
1728
|
+
const drawPoints = (
|
|
1729
|
+
getPointSizeExtra,
|
|
1730
|
+
getNumPoints,
|
|
1731
|
+
getStateIndexBuffer,
|
|
1732
|
+
globalState = COLOR_NORMAL_IDX,
|
|
1733
|
+
getPointOpacityMax = getPointOpacityMaxBase,
|
|
1734
|
+
getPointOpacityScale = getPointOpacityScaleBase,
|
|
1735
|
+
) =>
|
|
1736
|
+
renderer.regl({
|
|
1737
|
+
frag: renderPointsAsSquares ? POINT_SIMPLE_FS : POINT_FS,
|
|
1738
|
+
vert: createVertexShader(globalState),
|
|
1739
|
+
|
|
1740
|
+
blend: {
|
|
1741
|
+
enable: !disableAlphaBlending,
|
|
1742
|
+
func: {
|
|
1743
|
+
// biome-ignore lint/style/useNamingConvention: Regl specific
|
|
1744
|
+
srcRGB: 'src alpha',
|
|
1745
|
+
srcAlpha: 'one',
|
|
1746
|
+
// biome-ignore lint/style/useNamingConvention: Regl specific
|
|
1747
|
+
dstRGB: 'one minus src alpha',
|
|
1748
|
+
dstAlpha: 'one minus src alpha',
|
|
1749
|
+
},
|
|
1750
|
+
},
|
|
1751
|
+
|
|
1752
|
+
depth: { enable: false },
|
|
1753
|
+
|
|
1754
|
+
attributes: {
|
|
1755
|
+
stateIndex: {
|
|
1756
|
+
buffer: getStateIndexBuffer,
|
|
1757
|
+
size: 2,
|
|
1758
|
+
},
|
|
1759
|
+
},
|
|
1760
|
+
|
|
1761
|
+
uniforms: {
|
|
1762
|
+
antiAliasing: getAntiAliasing,
|
|
1763
|
+
resolution: getResolution,
|
|
1764
|
+
modelViewProjection: getModelViewProjection,
|
|
1765
|
+
devicePixelRatio: getDevicePixelRatio,
|
|
1766
|
+
pointScale: () => getPointScale(),
|
|
1767
|
+
encodingTex: getEncodingTex,
|
|
1768
|
+
encodingTexRes: getEncodingTexRes,
|
|
1769
|
+
encodingTexEps: getEncodingTexEps,
|
|
1770
|
+
pointOpacityMax: getPointOpacityMax,
|
|
1771
|
+
pointOpacityScale: getPointOpacityScale,
|
|
1772
|
+
pointSizeExtra: getPointSizeExtra,
|
|
1773
|
+
globalState,
|
|
1774
|
+
colorTex: getColorTex,
|
|
1775
|
+
colorTexRes: getColorTexRes,
|
|
1776
|
+
colorTexEps: getColorTexEps,
|
|
1777
|
+
stateTex: getStateTex,
|
|
1778
|
+
stateTexRes: getStateTexRes,
|
|
1779
|
+
stateTexEps: getStateTexEps,
|
|
1780
|
+
isColoredByZ: getIsColoredByZ,
|
|
1781
|
+
isColoredByW: getIsColoredByW,
|
|
1782
|
+
isOpacityByZ: getIsOpacityByZ,
|
|
1783
|
+
isOpacityByW: getIsOpacityByW,
|
|
1784
|
+
isOpacityByDensity: getIsOpacityByDensity,
|
|
1785
|
+
isSizedByZ: getIsSizedByZ,
|
|
1786
|
+
isSizedByW: getIsSizedByW,
|
|
1787
|
+
isPixelAligned: getIsPixelAligned,
|
|
1788
|
+
colorMultiplicator: getColorMultiplicator,
|
|
1789
|
+
opacityMultiplicator: getOpacityMultiplicator,
|
|
1790
|
+
opacityDensity: getOpacityDensity,
|
|
1791
|
+
sizeMultiplicator: getSizeMultiplicator,
|
|
1792
|
+
numColorStates: COLOR_NUM_STATES,
|
|
1793
|
+
drawingBufferWidth: (context) => context.drawingBufferWidth,
|
|
1794
|
+
drawingBufferHeight: (context) => context.drawingBufferHeight,
|
|
1795
|
+
},
|
|
1796
|
+
|
|
1797
|
+
count: getNumPoints,
|
|
1798
|
+
|
|
1799
|
+
primitive: 'points',
|
|
1800
|
+
});
|
|
1801
|
+
|
|
1802
|
+
const drawPointBodies = drawPoints(
|
|
1803
|
+
getNormalPointSizeExtra,
|
|
1804
|
+
getNormalNumPoints,
|
|
1805
|
+
getNormalPointsIndexBuffer,
|
|
1806
|
+
);
|
|
1807
|
+
|
|
1808
|
+
const drawHoveredPoint = drawPoints(
|
|
1809
|
+
getNormalPointSizeExtra,
|
|
1810
|
+
() => 1,
|
|
1811
|
+
() => hoveredPointIndexBuffer,
|
|
1812
|
+
COLOR_HOVER_IDX,
|
|
1813
|
+
() => 1,
|
|
1814
|
+
() => 1,
|
|
1815
|
+
);
|
|
1816
|
+
|
|
1817
|
+
const drawSelectedPointOutlines = drawPoints(
|
|
1818
|
+
() => (pointSizeSelected + pointOutlineWidth * 2) * window.devicePixelRatio,
|
|
1819
|
+
getSelectedNumPoints,
|
|
1820
|
+
getSelectedPointsIndexBuffer,
|
|
1821
|
+
COLOR_ACTIVE_IDX,
|
|
1822
|
+
() => 1,
|
|
1823
|
+
() => 1,
|
|
1824
|
+
);
|
|
1825
|
+
|
|
1826
|
+
const drawSelectedPointInnerBorder = drawPoints(
|
|
1827
|
+
() => (pointSizeSelected + pointOutlineWidth) * window.devicePixelRatio,
|
|
1828
|
+
getSelectedNumPoints,
|
|
1829
|
+
getSelectedPointsIndexBuffer,
|
|
1830
|
+
COLOR_BG_IDX,
|
|
1831
|
+
() => 1,
|
|
1832
|
+
() => 1,
|
|
1833
|
+
);
|
|
1834
|
+
|
|
1835
|
+
const drawSelectedPointBodies = drawPoints(
|
|
1836
|
+
() => pointSizeSelected * window.devicePixelRatio,
|
|
1837
|
+
getSelectedNumPoints,
|
|
1838
|
+
getSelectedPointsIndexBuffer,
|
|
1839
|
+
COLOR_ACTIVE_IDX,
|
|
1840
|
+
() => 1,
|
|
1841
|
+
() => 1,
|
|
1842
|
+
);
|
|
1843
|
+
|
|
1844
|
+
const drawSelectedPoints = () => {
|
|
1845
|
+
drawSelectedPointOutlines();
|
|
1846
|
+
drawSelectedPointInnerBorder();
|
|
1847
|
+
drawSelectedPointBodies();
|
|
1848
|
+
};
|
|
1849
|
+
|
|
1850
|
+
const drawBackgroundImage = renderer.regl({
|
|
1851
|
+
frag: BG_FS,
|
|
1852
|
+
vert: BG_VS,
|
|
1853
|
+
|
|
1854
|
+
attributes: {
|
|
1855
|
+
position: [0, 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0],
|
|
1856
|
+
},
|
|
1857
|
+
|
|
1858
|
+
uniforms: {
|
|
1859
|
+
modelViewProjection: getModelViewProjection,
|
|
1860
|
+
texture: getBackgroundImage,
|
|
1861
|
+
},
|
|
1862
|
+
|
|
1863
|
+
count: 6,
|
|
1864
|
+
});
|
|
1865
|
+
|
|
1866
|
+
const drawLassoPolygon = renderer.regl({
|
|
1867
|
+
vert: `
|
|
1868
|
+
precision mediump float;
|
|
1869
|
+
uniform mat4 modelViewProjection;
|
|
1870
|
+
attribute vec2 position;
|
|
1871
|
+
void main () {
|
|
1872
|
+
gl_Position = modelViewProjection * vec4(position, 0, 1);
|
|
1873
|
+
}`,
|
|
1874
|
+
|
|
1875
|
+
frag: `
|
|
1876
|
+
precision mediump float;
|
|
1877
|
+
uniform vec4 color;
|
|
1878
|
+
void main () {
|
|
1879
|
+
gl_FragColor = vec4(color.rgb, 0.2);
|
|
1880
|
+
}`,
|
|
1881
|
+
|
|
1882
|
+
depth: { enable: false },
|
|
1883
|
+
|
|
1884
|
+
blend: {
|
|
1885
|
+
enable: true,
|
|
1886
|
+
func: {
|
|
1887
|
+
// biome-ignore lint/style/useNamingConvention: Regl specific
|
|
1888
|
+
srcRGB: 'src alpha',
|
|
1889
|
+
srcAlpha: 'one',
|
|
1890
|
+
// biome-ignore lint/style/useNamingConvention: Regl specific
|
|
1891
|
+
dstRGB: 'one minus src alpha',
|
|
1892
|
+
dstAlpha: 'one minus src alpha',
|
|
1893
|
+
},
|
|
1894
|
+
},
|
|
1895
|
+
|
|
1896
|
+
attributes: {
|
|
1897
|
+
position: () => lassoPointsCurr,
|
|
1898
|
+
},
|
|
1899
|
+
|
|
1900
|
+
uniforms: {
|
|
1901
|
+
modelViewProjection: getModelViewProjection,
|
|
1902
|
+
color: () => lassoColor,
|
|
1903
|
+
},
|
|
1904
|
+
|
|
1905
|
+
elements: () => earcut(lasso.getPoints()),
|
|
1906
|
+
});
|
|
1907
|
+
|
|
1908
|
+
const drawReticle = () => {
|
|
1909
|
+
if (!(hoveredPoint >= 0)) {
|
|
1910
|
+
return;
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
const [x, y] = points[hoveredPoint].slice(0, 2);
|
|
1914
|
+
|
|
1915
|
+
// Homogeneous coordinates of the point
|
|
1916
|
+
const v = [x, y, 0, 1];
|
|
1917
|
+
|
|
1918
|
+
// We have to calculate the model-view-projection matrix outside of the
|
|
1919
|
+
// shader as we actually don't want the model, view, or projection of the
|
|
1920
|
+
// line view space to change such that the reticle is visualized across the
|
|
1921
|
+
// entire view container and not within the view of the scatterplot
|
|
1922
|
+
mat4.multiply(
|
|
1923
|
+
scratch,
|
|
1924
|
+
projection,
|
|
1925
|
+
mat4.multiply(scratch, camera.view, model),
|
|
1926
|
+
);
|
|
1927
|
+
|
|
1928
|
+
vec4.transformMat4(v, v, scratch);
|
|
1929
|
+
|
|
1930
|
+
reticleHLine.setPoints([-1, v[1], 1, v[1]]);
|
|
1931
|
+
reticleVLine.setPoints([v[0], 1, v[0], -1]);
|
|
1932
|
+
|
|
1933
|
+
reticleHLine.draw();
|
|
1934
|
+
reticleVLine.draw();
|
|
1935
|
+
|
|
1936
|
+
// Draw outer outline
|
|
1937
|
+
drawPoints(
|
|
1938
|
+
() =>
|
|
1939
|
+
(pointSizeSelected + pointOutlineWidth * 2) * window.devicePixelRatio,
|
|
1940
|
+
() => 1,
|
|
1941
|
+
hoveredPointIndexBuffer,
|
|
1942
|
+
COLOR_ACTIVE_IDX,
|
|
1943
|
+
)();
|
|
1944
|
+
|
|
1945
|
+
// Draw inner outline
|
|
1946
|
+
drawPoints(
|
|
1947
|
+
() => (pointSizeSelected + pointOutlineWidth) * window.devicePixelRatio,
|
|
1948
|
+
() => 1,
|
|
1949
|
+
hoveredPointIndexBuffer,
|
|
1950
|
+
COLOR_BG_IDX,
|
|
1951
|
+
)();
|
|
1952
|
+
};
|
|
1953
|
+
|
|
1954
|
+
const createPointIndex = (numNewPoints) => {
|
|
1955
|
+
const index = new Float32Array(numNewPoints * 2);
|
|
1956
|
+
|
|
1957
|
+
let j = 0;
|
|
1958
|
+
for (let i = 0; i < numNewPoints; ++i) {
|
|
1959
|
+
const texCoord = indexToStateTexCoord(i);
|
|
1960
|
+
index[j] = texCoord[0]; // x
|
|
1961
|
+
index[j + 1] = texCoord[1]; // y
|
|
1962
|
+
j += 2;
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
return index;
|
|
1966
|
+
};
|
|
1967
|
+
|
|
1968
|
+
const createStateTexture = (newPoints, dataTypes = {}) => {
|
|
1969
|
+
const numNewPoints = newPoints.length;
|
|
1970
|
+
stateTexRes = Math.max(2, Math.ceil(Math.sqrt(numNewPoints)));
|
|
1971
|
+
stateTexEps = 0.5 / stateTexRes;
|
|
1972
|
+
const data = new Float32Array(stateTexRes ** 2 * 4);
|
|
1973
|
+
|
|
1974
|
+
let zIsInts = true;
|
|
1975
|
+
let wIsInts = true;
|
|
1976
|
+
|
|
1977
|
+
let k = 0;
|
|
1978
|
+
let z = 0;
|
|
1979
|
+
let w = 0;
|
|
1980
|
+
for (let i = 0; i < numNewPoints; ++i) {
|
|
1981
|
+
k = i * 4;
|
|
1982
|
+
|
|
1983
|
+
data[k] = newPoints[i][0]; // x
|
|
1984
|
+
data[k + 1] = newPoints[i][1]; // y
|
|
1985
|
+
|
|
1986
|
+
z = newPoints[i][2] || 0;
|
|
1987
|
+
w = newPoints[i][3] || 0;
|
|
1988
|
+
|
|
1989
|
+
data[k + 2] = z; // z: value 1
|
|
1990
|
+
data[k + 3] = w; // w: value 2
|
|
1991
|
+
zIsInts &&= Number.isInteger(z);
|
|
1992
|
+
wIsInts &&= Number.isInteger(w);
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1995
|
+
if (dataTypes.z && VALUE_ZW_DATA_TYPES.includes(dataTypes.z)) {
|
|
1996
|
+
valueZDataType = dataTypes.z;
|
|
1997
|
+
} else {
|
|
1998
|
+
valueZDataType = zIsInts ? CATEGORICAL : CONTINUOUS;
|
|
1999
|
+
}
|
|
2000
|
+
|
|
2001
|
+
if (dataTypes.w && VALUE_ZW_DATA_TYPES.includes(dataTypes.w)) {
|
|
2002
|
+
valueWDataType = dataTypes.w;
|
|
2003
|
+
} else {
|
|
2004
|
+
valueWDataType = wIsInts ? CATEGORICAL : CONTINUOUS;
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
return renderer.regl.texture({
|
|
2008
|
+
data,
|
|
2009
|
+
shape: [stateTexRes, stateTexRes, 4],
|
|
2010
|
+
type: 'float',
|
|
2011
|
+
});
|
|
2012
|
+
};
|
|
2013
|
+
|
|
2014
|
+
const cachePoints = (newPoints, dataTypes = {}) => {
|
|
2015
|
+
if (!stateTex) {
|
|
2016
|
+
return false;
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
if (isTransitioning) {
|
|
2020
|
+
const tmp = prevStateTex;
|
|
2021
|
+
prevStateTex = tmpStateTex;
|
|
2022
|
+
tmp.destroy();
|
|
2023
|
+
} else {
|
|
2024
|
+
prevStateTex = stateTex;
|
|
2025
|
+
}
|
|
2026
|
+
|
|
2027
|
+
tmpStateTex = createStateTexture(newPoints, dataTypes);
|
|
2028
|
+
tmpStateBuffer = renderer.regl.framebuffer({
|
|
2029
|
+
color: tmpStateTex,
|
|
2030
|
+
depth: false,
|
|
2031
|
+
stencil: false,
|
|
2032
|
+
});
|
|
2033
|
+
stateTex = undefined;
|
|
2034
|
+
|
|
2035
|
+
return true;
|
|
2036
|
+
};
|
|
2037
|
+
|
|
2038
|
+
const hasCachedPoints = () => Boolean(prevStateTex && tmpStateTex);
|
|
2039
|
+
|
|
2040
|
+
const clearCachedPoints = () => {
|
|
2041
|
+
if (prevStateTex) {
|
|
2042
|
+
prevStateTex.destroy();
|
|
2043
|
+
prevStateTex = undefined;
|
|
2044
|
+
}
|
|
2045
|
+
|
|
2046
|
+
if (tmpStateTex) {
|
|
2047
|
+
tmpStateTex.destroy();
|
|
2048
|
+
tmpStateTex = undefined;
|
|
2049
|
+
}
|
|
2050
|
+
};
|
|
2051
|
+
|
|
2052
|
+
const setPoints = (newPoints, options = {}) =>
|
|
2053
|
+
new Promise((resolve) => {
|
|
2054
|
+
isPointsDrawn = false;
|
|
2055
|
+
|
|
2056
|
+
const preventFilterReset =
|
|
2057
|
+
options?.preventFilterReset && newPoints.length === numPoints;
|
|
2058
|
+
|
|
2059
|
+
numPoints = newPoints.length;
|
|
2060
|
+
numPointsInView = numPoints;
|
|
2061
|
+
|
|
2062
|
+
if (stateTex) {
|
|
2063
|
+
stateTex.destroy();
|
|
2064
|
+
}
|
|
2065
|
+
stateTex = createStateTexture(newPoints, {
|
|
2066
|
+
z: options.zDataType,
|
|
2067
|
+
w: options.wDataType,
|
|
2068
|
+
});
|
|
2069
|
+
|
|
2070
|
+
if (!preventFilterReset) {
|
|
2071
|
+
normalPointsIndexBuffer({
|
|
2072
|
+
usage: 'static',
|
|
2073
|
+
type: 'float',
|
|
2074
|
+
data: createPointIndex(numPoints),
|
|
2075
|
+
});
|
|
2076
|
+
}
|
|
2077
|
+
|
|
2078
|
+
createKdbush(options.spatialIndex || newPoints, {
|
|
2079
|
+
useWorker: spatialIndexUseWorker,
|
|
2080
|
+
})
|
|
2081
|
+
.then((newSearchIndex) => {
|
|
2082
|
+
spatialIndex = newSearchIndex;
|
|
2083
|
+
points = newPoints;
|
|
2084
|
+
|
|
2085
|
+
isPointsDrawn = true;
|
|
2086
|
+
})
|
|
2087
|
+
.then(resolve);
|
|
2088
|
+
});
|
|
2089
|
+
|
|
2090
|
+
const cacheCamera = (newTarget, newDistance) => {
|
|
2091
|
+
cameraZoomTargetStart = camera.target;
|
|
2092
|
+
cameraZoomTargetEnd = newTarget;
|
|
2093
|
+
cameraZoomDistanceStart = camera.distance[0];
|
|
2094
|
+
cameraZoomDistanceEnd = newDistance;
|
|
2095
|
+
};
|
|
2096
|
+
|
|
2097
|
+
const hasCachedCamera = () =>
|
|
2098
|
+
Boolean(
|
|
2099
|
+
cameraZoomTargetStart !== undefined &&
|
|
2100
|
+
cameraZoomTargetEnd !== undefined &&
|
|
2101
|
+
cameraZoomDistanceStart !== undefined &&
|
|
2102
|
+
cameraZoomDistanceEnd !== undefined,
|
|
2103
|
+
);
|
|
2104
|
+
|
|
2105
|
+
const clearCachedCamera = () => {
|
|
2106
|
+
cameraZoomTargetStart = undefined;
|
|
2107
|
+
cameraZoomTargetEnd = undefined;
|
|
2108
|
+
cameraZoomDistanceStart = undefined;
|
|
2109
|
+
cameraZoomDistanceEnd = undefined;
|
|
2110
|
+
};
|
|
2111
|
+
|
|
2112
|
+
const getPointConnectionColorIndices = (curvePoints) => {
|
|
2113
|
+
const colorEncoding =
|
|
2114
|
+
pointConnectionColorBy === 'inherit' ? colorBy : pointConnectionColorBy;
|
|
2115
|
+
|
|
2116
|
+
if (colorEncoding === 'segment') {
|
|
2117
|
+
const maxColorIdx = pointConnectionColor.length - 1;
|
|
2118
|
+
if (maxColorIdx < 1) {
|
|
2119
|
+
return [];
|
|
2120
|
+
}
|
|
2121
|
+
return curvePoints.reduce((colorIndices, curve, index) => {
|
|
2122
|
+
let totalLength = 0;
|
|
2123
|
+
const segLengths = [];
|
|
2124
|
+
// Compute the total length of the line
|
|
2125
|
+
for (let i = 2; i < curve.length; i += 2) {
|
|
2126
|
+
const segLength = Math.sqrt(
|
|
2127
|
+
(curve[i - 2] - curve[i]) ** 2 + (curve[i - 1] - curve[i + 1]) ** 2,
|
|
2128
|
+
);
|
|
2129
|
+
segLengths.push(segLength);
|
|
2130
|
+
totalLength += segLength;
|
|
2131
|
+
}
|
|
2132
|
+
colorIndices[index] = [0];
|
|
2133
|
+
let cumLength = 0;
|
|
2134
|
+
// Assign the color index based on the cumulative length
|
|
2135
|
+
for (let i = 0; i < curve.length / 2 - 1; i++) {
|
|
2136
|
+
cumLength += segLengths[i];
|
|
2137
|
+
// The `4` comes from the fact that we have 4 color states:
|
|
2138
|
+
// normal, active, hover, and background
|
|
2139
|
+
colorIndices[index].push(
|
|
2140
|
+
Math.floor((cumLength / totalLength) * maxColorIdx) * 4,
|
|
2141
|
+
);
|
|
2142
|
+
}
|
|
2143
|
+
// The `4` comes from the fact that we have 4 color states:
|
|
2144
|
+
// normal, active, hover, and background
|
|
2145
|
+
// colorIndices[index] = rangeMap(
|
|
2146
|
+
// curve.length,
|
|
2147
|
+
// (i) => Math.floor((i / (curve.length - 1)) * maxColorIdx) * 4
|
|
2148
|
+
// );
|
|
2149
|
+
return colorIndices;
|
|
2150
|
+
}, []);
|
|
2151
|
+
}
|
|
2152
|
+
|
|
2153
|
+
if (colorEncoding) {
|
|
2154
|
+
const encodingIdx = getEncodingIdx(colorEncoding);
|
|
2155
|
+
const encodingValueToIdx = getEncodingValueToIdx(
|
|
2156
|
+
getEncodingDataType(colorEncoding),
|
|
2157
|
+
pointConnectionColorBy === 'inherit'
|
|
2158
|
+
? pointColor
|
|
2159
|
+
: pointConnectionColor,
|
|
2160
|
+
);
|
|
2161
|
+
return pointConnectionMap.reduce(
|
|
2162
|
+
(colorIndices, [index, referencePoint]) => {
|
|
2163
|
+
// The `4` comes from the fact that we have 4 color states:
|
|
2164
|
+
// normal, active, hover, and background
|
|
2165
|
+
colorIndices[index] =
|
|
2166
|
+
encodingValueToIdx(referencePoint[encodingIdx]) * 4;
|
|
2167
|
+
return colorIndices;
|
|
2168
|
+
},
|
|
2169
|
+
[],
|
|
2170
|
+
);
|
|
2171
|
+
}
|
|
2172
|
+
|
|
2173
|
+
return new Array(pointConnectionMap.length).fill(0);
|
|
2174
|
+
};
|
|
2175
|
+
|
|
2176
|
+
const getPointConnectionOpacities = () => {
|
|
2177
|
+
const opacityEncoding =
|
|
2178
|
+
pointConnectionOpacityBy === 'inherit'
|
|
2179
|
+
? opacityBy
|
|
2180
|
+
: pointConnectionOpacityBy;
|
|
2181
|
+
|
|
2182
|
+
if (opacityEncoding === 'segment') {
|
|
2183
|
+
const maxOpacityIdx = pointConnectionOpacity.length - 1;
|
|
2184
|
+
if (maxOpacityIdx < 1) {
|
|
2185
|
+
return [];
|
|
2186
|
+
}
|
|
2187
|
+
return pointConnectionMap.reduce(
|
|
2188
|
+
(opacities, [index, _referencePoint, length]) => {
|
|
2189
|
+
opacities[index] = rangeMap(
|
|
2190
|
+
length,
|
|
2191
|
+
(i) =>
|
|
2192
|
+
pointConnectionOpacity[
|
|
2193
|
+
Math.floor((i / (length - 1)) * maxOpacityIdx)
|
|
2194
|
+
],
|
|
2195
|
+
);
|
|
2196
|
+
return opacities;
|
|
2197
|
+
},
|
|
2198
|
+
[],
|
|
2199
|
+
);
|
|
2200
|
+
}
|
|
2201
|
+
|
|
2202
|
+
if (opacityEncoding) {
|
|
2203
|
+
const encodingIdx = getEncodingIdx(opacityEncoding);
|
|
2204
|
+
const encodingRangeMap =
|
|
2205
|
+
pointConnectionOpacityBy === 'inherit'
|
|
2206
|
+
? opacity
|
|
2207
|
+
: pointConnectionOpacity;
|
|
2208
|
+
const encodingValueToIdx = getEncodingValueToIdx(
|
|
2209
|
+
getEncodingDataType(opacityEncoding),
|
|
2210
|
+
encodingRangeMap,
|
|
2211
|
+
);
|
|
2212
|
+
return pointConnectionMap.reduce((opacities, [index, referencePoint]) => {
|
|
2213
|
+
opacities[index] =
|
|
2214
|
+
encodingRangeMap[encodingValueToIdx(referencePoint[encodingIdx])];
|
|
2215
|
+
return opacities;
|
|
2216
|
+
}, []);
|
|
2217
|
+
}
|
|
2218
|
+
|
|
2219
|
+
return undefined;
|
|
2220
|
+
};
|
|
2221
|
+
|
|
2222
|
+
const getPointConnectionWidths = () => {
|
|
2223
|
+
const sizeEncoding =
|
|
2224
|
+
pointConnectionSizeBy === 'inherit' ? sizeBy : pointConnectionSizeBy;
|
|
2225
|
+
|
|
2226
|
+
if (sizeEncoding === 'segment') {
|
|
2227
|
+
const maxSizeIdx = pointConnectionSize.length - 1;
|
|
2228
|
+
if (maxSizeIdx < 1) {
|
|
2229
|
+
return [];
|
|
2230
|
+
}
|
|
2231
|
+
return pointConnectionMap.reduce(
|
|
2232
|
+
(widths, [index, _referencePoint, length]) => {
|
|
2233
|
+
widths[index] = rangeMap(
|
|
2234
|
+
length,
|
|
2235
|
+
(i) =>
|
|
2236
|
+
pointConnectionSize[Math.floor((i / (length - 1)) * maxSizeIdx)],
|
|
2237
|
+
);
|
|
2238
|
+
return widths;
|
|
2239
|
+
},
|
|
2240
|
+
[],
|
|
2241
|
+
);
|
|
2242
|
+
}
|
|
2243
|
+
|
|
2244
|
+
if (sizeEncoding) {
|
|
2245
|
+
const encodingIdx = getEncodingIdx(sizeEncoding);
|
|
2246
|
+
const encodingRangeMap =
|
|
2247
|
+
pointConnectionSizeBy === 'inherit' ? pointSize : pointConnectionSize;
|
|
2248
|
+
const encodingValueToIdx = getEncodingValueToIdx(
|
|
2249
|
+
getEncodingDataType(sizeEncoding),
|
|
2250
|
+
encodingRangeMap,
|
|
2251
|
+
);
|
|
2252
|
+
return pointConnectionMap.reduce((widths, [index, referencePoint]) => {
|
|
2253
|
+
widths[index] =
|
|
2254
|
+
encodingRangeMap[encodingValueToIdx(referencePoint[encodingIdx])];
|
|
2255
|
+
return widths;
|
|
2256
|
+
}, []);
|
|
2257
|
+
}
|
|
2258
|
+
|
|
2259
|
+
return undefined;
|
|
2260
|
+
};
|
|
2261
|
+
|
|
2262
|
+
const setPointConnectionMap = (curvePoints) => {
|
|
2263
|
+
pointConnectionMap = [];
|
|
2264
|
+
|
|
2265
|
+
let cumLinePoints = 0;
|
|
2266
|
+
Object.keys(curvePoints).forEach((id, index) => {
|
|
2267
|
+
pointConnectionMap[id] = [
|
|
2268
|
+
index,
|
|
2269
|
+
curvePoints[id].reference,
|
|
2270
|
+
curvePoints[id].length / 2,
|
|
2271
|
+
// Used for offsetting in the buffer manipulations on
|
|
2272
|
+
// hovering and selecting
|
|
2273
|
+
cumLinePoints,
|
|
2274
|
+
];
|
|
2275
|
+
cumLinePoints += curvePoints[id].length / 2;
|
|
2276
|
+
});
|
|
2277
|
+
};
|
|
2278
|
+
|
|
2279
|
+
const setPointConnections = (newPoints) =>
|
|
2280
|
+
new Promise((resolve) => {
|
|
2281
|
+
pointConnections.setPoints([]);
|
|
2282
|
+
if (newPoints?.length > 0) {
|
|
2283
|
+
computingPointConnectionCurves = true;
|
|
2284
|
+
createSplineCurve(newPoints, {
|
|
2285
|
+
maxIntPointsPerSegment: pointConnectionMaxIntPointsPerSegment,
|
|
2286
|
+
tolerance: pointConnectionTolerance,
|
|
2287
|
+
}).then((curvePoints) => {
|
|
2288
|
+
setPointConnectionMap(curvePoints);
|
|
2289
|
+
const curvePointValues = Object.values(curvePoints);
|
|
2290
|
+
pointConnections.setPoints(
|
|
2291
|
+
curvePointValues.length === 1
|
|
2292
|
+
? curvePointValues[0]
|
|
2293
|
+
: curvePointValues,
|
|
2294
|
+
{
|
|
2295
|
+
colorIndices: getPointConnectionColorIndices(curvePointValues),
|
|
2296
|
+
opacities: getPointConnectionOpacities(curvePointValues),
|
|
2297
|
+
widths: getPointConnectionWidths(curvePointValues),
|
|
2298
|
+
},
|
|
2299
|
+
);
|
|
2300
|
+
computingPointConnectionCurves = false;
|
|
2301
|
+
resolve();
|
|
2302
|
+
});
|
|
2303
|
+
} else {
|
|
2304
|
+
resolve();
|
|
2305
|
+
}
|
|
2306
|
+
});
|
|
2307
|
+
|
|
2308
|
+
/**
|
|
2309
|
+
* Reset the point filter
|
|
2310
|
+
* @param {import('./types').ScatterplotMethodOptions['filter']}
|
|
2311
|
+
*/
|
|
2312
|
+
const unfilter = ({ preventEvent = false } = {}) => {
|
|
2313
|
+
isPointsFiltered = false;
|
|
2314
|
+
filteredPointsSet.clear();
|
|
2315
|
+
normalPointsIndexBuffer.subdata(createPointIndex(numPoints));
|
|
2316
|
+
|
|
2317
|
+
return new Promise((resolve) => {
|
|
2318
|
+
const finish = () => {
|
|
2319
|
+
pubSub.subscribe(
|
|
2320
|
+
'draw',
|
|
2321
|
+
() => {
|
|
2322
|
+
if (!preventEvent) {
|
|
2323
|
+
pubSub.publish('unfilter');
|
|
2324
|
+
}
|
|
2325
|
+
resolve();
|
|
2326
|
+
},
|
|
2327
|
+
1,
|
|
2328
|
+
);
|
|
2329
|
+
draw = true;
|
|
2330
|
+
};
|
|
2331
|
+
|
|
2332
|
+
// Update point connections
|
|
2333
|
+
if (showPointConnections || hasPointConnections(points[0])) {
|
|
2334
|
+
setPointConnections(getPoints()).then(() => {
|
|
2335
|
+
if (!preventEvent) {
|
|
2336
|
+
pubSub.publish('pointConnectionsDraw');
|
|
2337
|
+
}
|
|
2338
|
+
finish();
|
|
2339
|
+
});
|
|
2340
|
+
} else {
|
|
2341
|
+
finish();
|
|
2342
|
+
}
|
|
2343
|
+
});
|
|
2344
|
+
};
|
|
2345
|
+
|
|
2346
|
+
/**
|
|
2347
|
+
* Filter down to a set of points
|
|
2348
|
+
* @param {number | number[]} pointIdxs
|
|
2349
|
+
* @param {import('./types').ScatterplotMethodOptions['filter']}
|
|
2350
|
+
*/
|
|
2351
|
+
const filter = (pointIdxs, { preventEvent = false } = {}) => {
|
|
2352
|
+
isPointsFiltered = true;
|
|
2353
|
+
filteredPointsSet.clear();
|
|
2354
|
+
|
|
2355
|
+
const pointIdxsArray = Array.isArray(pointIdxs) ? pointIdxs : [pointIdxs];
|
|
2356
|
+
const filteredPoints = [];
|
|
2357
|
+
const filteredPointsBuffer = [];
|
|
2358
|
+
const filteredSelectedPoints = [];
|
|
2359
|
+
|
|
2360
|
+
for (const pointIdx of pointIdxsArray) {
|
|
2361
|
+
if (!Number.isFinite(pointIdx) || pointIdx < 0 || pointIdx >= numPoints) {
|
|
2362
|
+
// Skip invalid filtered points
|
|
2363
|
+
continue;
|
|
2364
|
+
}
|
|
2365
|
+
|
|
2366
|
+
filteredPoints.push(pointIdx);
|
|
2367
|
+
filteredPointsSet.add(pointIdx);
|
|
2368
|
+
|
|
2369
|
+
if (selectedPointsSet.has(pointIdx)) {
|
|
2370
|
+
filteredSelectedPoints.push(pointIdx);
|
|
2371
|
+
}
|
|
2372
|
+
}
|
|
2373
|
+
|
|
2374
|
+
const sortedFilteredPoints = insertionSort([...filteredPoints]);
|
|
2375
|
+
|
|
2376
|
+
for (const pointIdx of sortedFilteredPoints) {
|
|
2377
|
+
filteredPointsBuffer.push.apply(
|
|
2378
|
+
filteredPointsBuffer,
|
|
2379
|
+
indexToStateTexCoord(pointIdx),
|
|
2380
|
+
);
|
|
2381
|
+
}
|
|
2382
|
+
|
|
2383
|
+
// Update the normal points index buffers
|
|
2384
|
+
normalPointsIndexBuffer.subdata(filteredPointsBuffer);
|
|
2385
|
+
|
|
2386
|
+
// Update selection
|
|
2387
|
+
select(filteredSelectedPoints, { preventEvent });
|
|
2388
|
+
|
|
2389
|
+
// Unset any potentially hovered point
|
|
2390
|
+
if (!filteredPointsSet.has(hoveredPoint)) {
|
|
2391
|
+
hover(-1, { preventEvent });
|
|
2392
|
+
}
|
|
2393
|
+
|
|
2394
|
+
return new Promise((resolve) => {
|
|
2395
|
+
const finish = () => {
|
|
2396
|
+
pubSub.subscribe(
|
|
2397
|
+
'draw',
|
|
2398
|
+
() => {
|
|
2399
|
+
if (!preventEvent) {
|
|
2400
|
+
pubSub.publish('filter', { points: filteredPoints });
|
|
2401
|
+
}
|
|
2402
|
+
resolve();
|
|
2403
|
+
},
|
|
2404
|
+
1,
|
|
2405
|
+
);
|
|
2406
|
+
draw = true;
|
|
2407
|
+
};
|
|
2408
|
+
|
|
2409
|
+
// Update point connections
|
|
2410
|
+
if (showPointConnections || hasPointConnections(points[0])) {
|
|
2411
|
+
setPointConnections(getPoints()).then(() => {
|
|
2412
|
+
if (!preventEvent) {
|
|
2413
|
+
pubSub.publish('pointConnectionsDraw');
|
|
2414
|
+
}
|
|
2415
|
+
// We have to re-apply the selection because the connections might
|
|
2416
|
+
// have changed
|
|
2417
|
+
select(filteredSelectedPoints, { preventEvent });
|
|
2418
|
+
finish();
|
|
2419
|
+
});
|
|
2420
|
+
} else {
|
|
2421
|
+
finish();
|
|
2422
|
+
}
|
|
2423
|
+
});
|
|
2424
|
+
};
|
|
2425
|
+
|
|
2426
|
+
const getPointsInView = () =>
|
|
2427
|
+
getPointsInBBox(
|
|
2428
|
+
bottomLeftNdc[0],
|
|
2429
|
+
bottomLeftNdc[1],
|
|
2430
|
+
topRightNdc[0],
|
|
2431
|
+
topRightNdc[1],
|
|
2432
|
+
);
|
|
2433
|
+
|
|
2434
|
+
const getNumPointsInView = () => {
|
|
2435
|
+
numPointsInView = getPointsInView().length;
|
|
2436
|
+
};
|
|
2437
|
+
|
|
2438
|
+
const getNumPointsInViewDb = throttleAndDebounce(
|
|
2439
|
+
getNumPointsInView,
|
|
2440
|
+
opacityByDensityDebounceTime,
|
|
2441
|
+
);
|
|
2442
|
+
|
|
2443
|
+
const tweenCamera = (t) => {
|
|
2444
|
+
const [xStart, yStart] = cameraZoomTargetStart;
|
|
2445
|
+
const [xEnd, yEnd] = cameraZoomTargetEnd;
|
|
2446
|
+
|
|
2447
|
+
const ti = 1.0 - t;
|
|
2448
|
+
|
|
2449
|
+
const targetX = xStart * ti + xEnd * t;
|
|
2450
|
+
const targetY = yStart * ti + yEnd * t;
|
|
2451
|
+
const distance = cameraZoomDistanceStart * ti + cameraZoomDistanceEnd * t;
|
|
2452
|
+
|
|
2453
|
+
camera.lookAt([targetX, targetY], distance);
|
|
2454
|
+
};
|
|
2455
|
+
|
|
2456
|
+
const isTransitioningPoints = () => hasCachedPoints();
|
|
2457
|
+
|
|
2458
|
+
const isTransitioningCamera = () => hasCachedCamera();
|
|
2459
|
+
|
|
2460
|
+
const tween = (duration, easing) => {
|
|
2461
|
+
if (!transitionStartTime) {
|
|
2462
|
+
transitionStartTime = performance.now();
|
|
2463
|
+
}
|
|
2464
|
+
|
|
2465
|
+
const dt = performance.now() - transitionStartTime;
|
|
2466
|
+
const t = clip(easing(dt / duration), 0, 1);
|
|
2467
|
+
|
|
2468
|
+
if (isTransitioningPoints()) {
|
|
2469
|
+
updatePoints({ t });
|
|
2470
|
+
}
|
|
2471
|
+
|
|
2472
|
+
if (isTransitioningCamera()) {
|
|
2473
|
+
tweenCamera(t);
|
|
2474
|
+
}
|
|
2475
|
+
|
|
2476
|
+
return dt < duration;
|
|
2477
|
+
};
|
|
2478
|
+
|
|
2479
|
+
const endTransition = () => {
|
|
2480
|
+
isTransitioning = false;
|
|
2481
|
+
transitionStartTime = null;
|
|
2482
|
+
transitionDuration = undefined;
|
|
2483
|
+
transitionEasing = undefined;
|
|
2484
|
+
showReticle = preTransitionShowReticle;
|
|
2485
|
+
|
|
2486
|
+
clearCachedPoints();
|
|
2487
|
+
clearCachedCamera();
|
|
2488
|
+
|
|
2489
|
+
pubSub.publish('transitionEnd');
|
|
2490
|
+
};
|
|
2491
|
+
|
|
2492
|
+
const startTransition = ({ duration = 500, easing = DEFAULT_EASING }) => {
|
|
2493
|
+
if (isTransitioning) {
|
|
2494
|
+
pubSub.publish('transitionEnd');
|
|
2495
|
+
}
|
|
2496
|
+
|
|
2497
|
+
isTransitioning = true;
|
|
2498
|
+
transitionStartTime = null;
|
|
2499
|
+
transitionDuration = duration;
|
|
2500
|
+
transitionEasing = isString(easing)
|
|
2501
|
+
? EASING_FNS[easing] || DEFAULT_EASING
|
|
2502
|
+
: easing;
|
|
2503
|
+
preTransitionShowReticle = showReticle;
|
|
2504
|
+
showReticle = false;
|
|
2505
|
+
|
|
2506
|
+
pubSub.publish('transitionStart');
|
|
2507
|
+
};
|
|
2508
|
+
|
|
2509
|
+
/**
|
|
2510
|
+
* @param {import('./types').Points} newPoints
|
|
2511
|
+
* @param {import('./types').ScatterplotMethodOptions['draw']} options
|
|
2512
|
+
* @returns {Promise<void>}
|
|
2513
|
+
*/
|
|
2514
|
+
const publicDraw = (newPoints, options = {}) => {
|
|
2515
|
+
if (isDestroyed) {
|
|
2516
|
+
return Promise.reject(new Error(ERROR_INSTANCE_IS_DESTROYED));
|
|
2517
|
+
}
|
|
2518
|
+
if (isDrawing) {
|
|
2519
|
+
return Promise.reject(new Error(ERROR_IS_DRAWING));
|
|
2520
|
+
}
|
|
2521
|
+
isDrawing = true;
|
|
2522
|
+
return toArrayOrientedPoints(newPoints).then((newPointsArray) =>
|
|
2523
|
+
new Promise((resolve) => {
|
|
2524
|
+
if (isDestroyed) {
|
|
2525
|
+
// In the special case where the instance was destroyed after
|
|
2526
|
+
// scatterplot.draw() was called but before toArrayOrientedPoints()
|
|
2527
|
+
// resolved, we will _not_ reject the promise as this would be
|
|
2528
|
+
// confusing. Instead we will immediately resolve and return.
|
|
2529
|
+
resolve();
|
|
2530
|
+
return;
|
|
2531
|
+
}
|
|
2532
|
+
|
|
2533
|
+
let pointsCached = false;
|
|
2534
|
+
|
|
2535
|
+
if (
|
|
2536
|
+
!options.preventFilterReset ||
|
|
2537
|
+
newPointsArray?.length !== numPoints
|
|
2538
|
+
) {
|
|
2539
|
+
isPointsFiltered = false;
|
|
2540
|
+
filteredPointsSet.clear();
|
|
2541
|
+
}
|
|
2542
|
+
|
|
2543
|
+
const drawPointConnections =
|
|
2544
|
+
newPointsArray &&
|
|
2545
|
+
hasPointConnections(newPointsArray[0]) &&
|
|
2546
|
+
(showPointConnections || options.showPointConnectionsOnce);
|
|
2547
|
+
|
|
2548
|
+
const { zDataType, wDataType } = options;
|
|
2549
|
+
|
|
2550
|
+
new Promise((resolveDraw) => {
|
|
2551
|
+
if (newPointsArray) {
|
|
2552
|
+
if (options.transition) {
|
|
2553
|
+
if (newPointsArray.length === numPoints) {
|
|
2554
|
+
pointsCached = cachePoints(newPointsArray, {
|
|
2555
|
+
z: zDataType,
|
|
2556
|
+
w: wDataType,
|
|
2557
|
+
});
|
|
2558
|
+
} else {
|
|
2559
|
+
// biome-ignore lint/suspicious/noConsole: This is a legitimately useful warning
|
|
2560
|
+
console.warn(
|
|
2561
|
+
'Cannot transition! The number of points between the previous and current draw call must be identical.',
|
|
2562
|
+
);
|
|
2563
|
+
}
|
|
2564
|
+
}
|
|
2565
|
+
|
|
2566
|
+
setPoints(newPointsArray, {
|
|
2567
|
+
zDataType,
|
|
2568
|
+
wDataType,
|
|
2569
|
+
preventFilterReset: options.preventFilterReset,
|
|
2570
|
+
spatialIndex: options.spatialIndex,
|
|
2571
|
+
}).then(() => {
|
|
2572
|
+
if (options.hover !== undefined) {
|
|
2573
|
+
hover(options.hover, { preventEvent: true });
|
|
2574
|
+
}
|
|
2575
|
+
|
|
2576
|
+
if (options.select !== undefined) {
|
|
2577
|
+
select(options.select, { preventEvent: true });
|
|
2578
|
+
}
|
|
2579
|
+
|
|
2580
|
+
if (options.filter !== undefined) {
|
|
2581
|
+
filter(options.filter, { preventEvent: true });
|
|
2582
|
+
}
|
|
2583
|
+
|
|
2584
|
+
if (drawPointConnections) {
|
|
2585
|
+
setPointConnections(newPointsArray)
|
|
2586
|
+
.then(() => {
|
|
2587
|
+
pubSub.publish('pointConnectionsDraw');
|
|
2588
|
+
draw = true;
|
|
2589
|
+
drawReticleOnce = options.showReticleOnce;
|
|
2590
|
+
})
|
|
2591
|
+
.then(() => resolve());
|
|
2592
|
+
} else {
|
|
2593
|
+
resolveDraw();
|
|
2594
|
+
}
|
|
2595
|
+
});
|
|
2596
|
+
} else {
|
|
2597
|
+
resolveDraw();
|
|
2598
|
+
}
|
|
2599
|
+
}).then(() => {
|
|
2600
|
+
if (options.transition && pointsCached) {
|
|
2601
|
+
if (drawPointConnections) {
|
|
2602
|
+
Promise.all([
|
|
2603
|
+
new Promise((resolveTransition) => {
|
|
2604
|
+
pubSub.subscribe(
|
|
2605
|
+
'transitionEnd',
|
|
2606
|
+
() => {
|
|
2607
|
+
// Point connects cannot be transitioned yet so we hide them during
|
|
2608
|
+
// the transition. Hence, we need to make sure we call `draw()` once
|
|
2609
|
+
// the transition has ended.
|
|
2610
|
+
draw = true;
|
|
2611
|
+
drawReticleOnce = options.showReticleOnce;
|
|
2612
|
+
resolveTransition();
|
|
2613
|
+
},
|
|
2614
|
+
1,
|
|
2615
|
+
);
|
|
2616
|
+
}),
|
|
2617
|
+
new Promise((resolveDraw) => {
|
|
2618
|
+
pubSub.subscribe('pointConnectionsDraw', resolveDraw, 1);
|
|
2619
|
+
}),
|
|
2620
|
+
]).then(() => resolve());
|
|
2621
|
+
} else {
|
|
2622
|
+
pubSub.subscribe(
|
|
2623
|
+
'transitionEnd',
|
|
2624
|
+
() => {
|
|
2625
|
+
// Point connects cannot be transitioned yet so we hide them during
|
|
2626
|
+
// the transition. Hence, we need to make sure we call `draw()` once
|
|
2627
|
+
// the transition has ended.
|
|
2628
|
+
draw = true;
|
|
2629
|
+
drawReticleOnce = options.showReticleOnce;
|
|
2630
|
+
resolve();
|
|
2631
|
+
},
|
|
2632
|
+
1,
|
|
2633
|
+
);
|
|
2634
|
+
}
|
|
2635
|
+
startTransition({
|
|
2636
|
+
duration: options.transitionDuration,
|
|
2637
|
+
easing: options.transitionEasing,
|
|
2638
|
+
});
|
|
2639
|
+
} else {
|
|
2640
|
+
if (drawPointConnections) {
|
|
2641
|
+
Promise.all([
|
|
2642
|
+
new Promise((resolveDraw) => {
|
|
2643
|
+
pubSub.subscribe('draw', resolveDraw, 1);
|
|
2644
|
+
}),
|
|
2645
|
+
new Promise((resolveDraw) => {
|
|
2646
|
+
pubSub.subscribe('pointConnectionsDraw', resolveDraw, 1);
|
|
2647
|
+
}),
|
|
2648
|
+
]).then(() => resolve());
|
|
2649
|
+
} else {
|
|
2650
|
+
pubSub.subscribe('draw', () => resolve(), 1);
|
|
2651
|
+
}
|
|
2652
|
+
draw = true;
|
|
2653
|
+
drawReticleOnce = options.showReticleOnce;
|
|
2654
|
+
}
|
|
2655
|
+
});
|
|
2656
|
+
}).finally(() => {
|
|
2657
|
+
isDrawing = false;
|
|
2658
|
+
}),
|
|
2659
|
+
);
|
|
2660
|
+
};
|
|
2661
|
+
|
|
2662
|
+
/**
|
|
2663
|
+
* Draw line-based annotations.
|
|
2664
|
+
* @param {import('./types').Annotation[]} newAnnotations
|
|
2665
|
+
* @returns {Promise<void>}
|
|
2666
|
+
*/
|
|
2667
|
+
const drawAnnotations = (newAnnotations) => {
|
|
2668
|
+
if (isDestroyed) {
|
|
2669
|
+
return Promise.reject(new ERROR_INSTANCE_IS_DESTROYED());
|
|
2670
|
+
}
|
|
2671
|
+
|
|
2672
|
+
isAnnotationsDrawn = false;
|
|
2673
|
+
|
|
2674
|
+
if (newAnnotations.length === 0) {
|
|
2675
|
+
return new Promise((resolve) => {
|
|
2676
|
+
annotations.clear();
|
|
2677
|
+
pubSub.subscribe('draw', resolve, 1);
|
|
2678
|
+
isAnnotationsDrawn = true;
|
|
2679
|
+
draw = true;
|
|
2680
|
+
});
|
|
2681
|
+
}
|
|
2682
|
+
|
|
2683
|
+
return new Promise((resolve) => {
|
|
2684
|
+
const newPoints = [];
|
|
2685
|
+
const newColors = new Map();
|
|
2686
|
+
const newColorIndices = [];
|
|
2687
|
+
const newWidths = [];
|
|
2688
|
+
|
|
2689
|
+
let maxNewColorIdx = -1;
|
|
2690
|
+
|
|
2691
|
+
const addColorAndWidth = (annotation) => {
|
|
2692
|
+
newWidths.push(annotation.lineWidth || annotationLineWidth);
|
|
2693
|
+
|
|
2694
|
+
const color = toRgba(annotation.lineColor || annotationLineColor, true);
|
|
2695
|
+
const colorId = `[${color.join(',')}]`;
|
|
2696
|
+
if (newColors.has(colorId)) {
|
|
2697
|
+
const { idx } = newColors.get(colorId);
|
|
2698
|
+
newColorIndices.push(idx);
|
|
2699
|
+
} else {
|
|
2700
|
+
const idx = ++maxNewColorIdx;
|
|
2701
|
+
newColors.set(colorId, { idx, color });
|
|
2702
|
+
newColorIndices.push(idx);
|
|
2703
|
+
}
|
|
2704
|
+
};
|
|
2705
|
+
|
|
2706
|
+
for (const annotation of newAnnotations) {
|
|
2707
|
+
if (isHorizontalLine(annotation)) {
|
|
2708
|
+
newPoints.push([
|
|
2709
|
+
annotation.x1 ?? -annotationHVLineLimit,
|
|
2710
|
+
annotation.y,
|
|
2711
|
+
annotation.x2 ?? annotationHVLineLimit,
|
|
2712
|
+
annotation.y,
|
|
2713
|
+
]);
|
|
2714
|
+
addColorAndWidth(annotation);
|
|
2715
|
+
continue;
|
|
2716
|
+
}
|
|
2717
|
+
|
|
2718
|
+
if (isVerticalLine(annotation)) {
|
|
2719
|
+
newPoints.push([
|
|
2720
|
+
annotation.x,
|
|
2721
|
+
annotation.y1 ?? -annotationHVLineLimit,
|
|
2722
|
+
annotation.x,
|
|
2723
|
+
annotation.y2 ?? annotationHVLineLimit,
|
|
2724
|
+
]);
|
|
2725
|
+
addColorAndWidth(annotation);
|
|
2726
|
+
continue;
|
|
2727
|
+
}
|
|
2728
|
+
|
|
2729
|
+
if (isRect(annotation)) {
|
|
2730
|
+
newPoints.push([
|
|
2731
|
+
annotation.x1,
|
|
2732
|
+
annotation.y1,
|
|
2733
|
+
annotation.x2,
|
|
2734
|
+
annotation.y1,
|
|
2735
|
+
annotation.x2,
|
|
2736
|
+
annotation.y2,
|
|
2737
|
+
annotation.x1,
|
|
2738
|
+
annotation.y2,
|
|
2739
|
+
annotation.x1,
|
|
2740
|
+
annotation.y1,
|
|
2741
|
+
]);
|
|
2742
|
+
addColorAndWidth(annotation);
|
|
2743
|
+
continue;
|
|
2744
|
+
}
|
|
2745
|
+
|
|
2746
|
+
if (isDomRect(annotation)) {
|
|
2747
|
+
newPoints.push([
|
|
2748
|
+
annotation.x,
|
|
2749
|
+
annotation.y,
|
|
2750
|
+
annotation.x + annotation.width,
|
|
2751
|
+
annotation.y,
|
|
2752
|
+
annotation.x + annotation.width,
|
|
2753
|
+
annotation.y + annotation.height,
|
|
2754
|
+
annotation.x,
|
|
2755
|
+
annotation.y + annotation.height,
|
|
2756
|
+
annotation.x,
|
|
2757
|
+
annotation.y,
|
|
2758
|
+
]);
|
|
2759
|
+
addColorAndWidth(annotation);
|
|
2760
|
+
continue;
|
|
2761
|
+
}
|
|
2762
|
+
|
|
2763
|
+
if (isPolygon(annotation)) {
|
|
2764
|
+
newPoints.push(annotation.vertices.flatMap(identity));
|
|
2765
|
+
addColorAndWidth(annotation);
|
|
2766
|
+
}
|
|
2767
|
+
}
|
|
2768
|
+
|
|
2769
|
+
annotations.setStyle({
|
|
2770
|
+
color: Array.from(newColors.values())
|
|
2771
|
+
.sort((a, b) => (a.idx > b.idx ? 1 : -1))
|
|
2772
|
+
.map(({ color }) => color),
|
|
2773
|
+
});
|
|
2774
|
+
annotations.setPoints(
|
|
2775
|
+
newPoints.length === 1 ? newPoints.flat() : newPoints,
|
|
2776
|
+
{
|
|
2777
|
+
colorIndices: newColorIndices,
|
|
2778
|
+
widths: newWidths,
|
|
2779
|
+
},
|
|
2780
|
+
);
|
|
2781
|
+
|
|
2782
|
+
pubSub.subscribe('draw', resolve, 1);
|
|
2783
|
+
isAnnotationsDrawn = true;
|
|
2784
|
+
draw = true;
|
|
2785
|
+
});
|
|
2786
|
+
};
|
|
2787
|
+
|
|
2788
|
+
/** @type {<F extends Function>(f: F) => (...args: Parameters<F>) => Promise<ReturnType<F>>} */
|
|
2789
|
+
const withDraw =
|
|
2790
|
+
(f) =>
|
|
2791
|
+
(...args) => {
|
|
2792
|
+
const out = f(...args);
|
|
2793
|
+
draw = true;
|
|
2794
|
+
return new Promise((resolve) => {
|
|
2795
|
+
pubSub.subscribe('draw', () => resolve(out), 1);
|
|
2796
|
+
});
|
|
2797
|
+
};
|
|
2798
|
+
|
|
2799
|
+
/**
|
|
2800
|
+
* Get the bounding box of a set of points.
|
|
2801
|
+
* @param {number[]} pointIdxs - A list of point indices
|
|
2802
|
+
* @returns {import('./types').Rect} The bounding box
|
|
2803
|
+
*/
|
|
2804
|
+
// biome-ignore lint/style/useNamingConvention: BBox stands for BoundingBox
|
|
2805
|
+
const getBBoxOfPoints = (pointIdxs) => {
|
|
2806
|
+
let xMin = Number.POSITIVE_INFINITY;
|
|
2807
|
+
let xMax = Number.NEGATIVE_INFINITY;
|
|
2808
|
+
let yMin = Number.POSITIVE_INFINITY;
|
|
2809
|
+
let yMax = Number.NEGATIVE_INFINITY;
|
|
2810
|
+
|
|
2811
|
+
for (const pointIdx of pointIdxs) {
|
|
2812
|
+
const [x, y] = points[pointIdx];
|
|
2813
|
+
xMin = Math.min(xMin, x);
|
|
2814
|
+
xMax = Math.max(xMax, x);
|
|
2815
|
+
yMin = Math.min(yMin, y);
|
|
2816
|
+
yMax = Math.max(yMax, y);
|
|
2817
|
+
}
|
|
2818
|
+
|
|
2819
|
+
return { x: xMin, y: yMin, width: xMax - xMin, height: yMax - yMin };
|
|
2820
|
+
};
|
|
2821
|
+
|
|
2822
|
+
/**
|
|
2823
|
+
* Zoom to an area specified as a rectangle
|
|
2824
|
+
* @param {import('./types').Rect} rect - The rectangle to zoom to in normalized device coordinates
|
|
2825
|
+
* @param {import('./types').ScatterplotMethodOptions['zoomToArea']} options
|
|
2826
|
+
* @returns {Promise<void>}
|
|
2827
|
+
*/
|
|
2828
|
+
const zoomToArea = (rect, options = {}) =>
|
|
2829
|
+
new Promise((resolve) => {
|
|
2830
|
+
const target = vec4
|
|
2831
|
+
.transformMat4(
|
|
2832
|
+
[],
|
|
2833
|
+
[rect.x + rect.width / 2, rect.y + rect.height / 2, 0, 0],
|
|
2834
|
+
model,
|
|
2835
|
+
)
|
|
2836
|
+
.slice(0, 2);
|
|
2837
|
+
|
|
2838
|
+
// Vertical field of view
|
|
2839
|
+
// The Arc Tangent is based on the original camera position. Otherwise
|
|
2840
|
+
// we would have to do `Math.atan(1 / camera.view[5])`
|
|
2841
|
+
// biome-ignore lint/style/useNamingConvention: FOV stands for field of view
|
|
2842
|
+
const vFOV = 2 * Math.atan(1);
|
|
2843
|
+
|
|
2844
|
+
const aspectRatio = viewAspectRatio / dataAspectRatio;
|
|
2845
|
+
|
|
2846
|
+
const distance =
|
|
2847
|
+
rect.height * aspectRatio >= rect.width
|
|
2848
|
+
? // Distance is based on the height of the bounding box
|
|
2849
|
+
rect.height / 2 / Math.tan(vFOV / 2)
|
|
2850
|
+
: // Distance is based on the width of the bounding box
|
|
2851
|
+
rect.width / 2 / Math.tan(vFOV / 2) / aspectRatio;
|
|
2852
|
+
|
|
2853
|
+
if (options.transition) {
|
|
2854
|
+
camera.config({ isFixed: true });
|
|
2855
|
+
cacheCamera(target, distance);
|
|
2856
|
+
pubSub.subscribe(
|
|
2857
|
+
'transitionEnd',
|
|
2858
|
+
() => {
|
|
2859
|
+
resolve();
|
|
2860
|
+
camera.config({ isFixed: cameraIsFixed });
|
|
2861
|
+
},
|
|
2862
|
+
1,
|
|
2863
|
+
);
|
|
2864
|
+
startTransition({
|
|
2865
|
+
duration: options.transitionDuration,
|
|
2866
|
+
easing: options.transitionEasing,
|
|
2867
|
+
});
|
|
2868
|
+
} else {
|
|
2869
|
+
camera.lookAt(target, distance);
|
|
2870
|
+
pubSub.subscribe('draw', resolve, 1);
|
|
2871
|
+
draw = true;
|
|
2872
|
+
}
|
|
2873
|
+
});
|
|
2874
|
+
|
|
2875
|
+
/**
|
|
2876
|
+
* Zoom to a set of points
|
|
2877
|
+
* @param {number[]} pointIdxs - A list of point indices
|
|
2878
|
+
* @param {import('./types').ScatterplotMethodOptions['zoomToPoints']} options
|
|
2879
|
+
* @returns {Promise<void>}
|
|
2880
|
+
*/
|
|
2881
|
+
const zoomToPoints = (pointIdxs, options = {}) => {
|
|
2882
|
+
if (!isPointsDrawn) {
|
|
2883
|
+
return Promise.reject(new Error(ERROR_POINTS_NOT_DRAWN));
|
|
2884
|
+
}
|
|
2885
|
+
const rect = getBBoxOfPoints(pointIdxs);
|
|
2886
|
+
const cX = rect.x + rect.width / 2;
|
|
2887
|
+
const cY = rect.y + rect.height / 2;
|
|
2888
|
+
|
|
2889
|
+
const pointSizeNdc = getPointSizeNdc();
|
|
2890
|
+
const scale = 1 + (options.padding || 0);
|
|
2891
|
+
|
|
2892
|
+
const w = Math.max(rect.width, pointSizeNdc) * scale;
|
|
2893
|
+
const h = Math.max(rect.height, pointSizeNdc) * scale;
|
|
2894
|
+
const x = cX - w / 2;
|
|
2895
|
+
const y = cY - h / 2;
|
|
2896
|
+
|
|
2897
|
+
return zoomToArea({ x, y, width: w, height: h }, options);
|
|
2898
|
+
};
|
|
2899
|
+
|
|
2900
|
+
/**
|
|
2901
|
+
* Zoom to a location specified in normalized devide coordinates.
|
|
2902
|
+
* @param {number[]} target - The camera target given in normalized device coordinates
|
|
2903
|
+
* @param {number} distance - The camera distance
|
|
2904
|
+
* @param {import('./types').ScatterplotMethodOptions['zoomToLocation']} options
|
|
2905
|
+
* @returns {Promise<void>}
|
|
2906
|
+
*/
|
|
2907
|
+
const zoomToLocation = (target, distance, options = {}) =>
|
|
2908
|
+
new Promise((resolve) => {
|
|
2909
|
+
if (options.transition) {
|
|
2910
|
+
camera.config({ isFixed: true });
|
|
2911
|
+
cacheCamera(target, distance);
|
|
2912
|
+
pubSub.subscribe(
|
|
2913
|
+
'transitionEnd',
|
|
2914
|
+
() => {
|
|
2915
|
+
resolve();
|
|
2916
|
+
camera.config({ isFixed: cameraIsFixed });
|
|
2917
|
+
},
|
|
2918
|
+
1,
|
|
2919
|
+
);
|
|
2920
|
+
startTransition({
|
|
2921
|
+
duration: options.transitionDuration,
|
|
2922
|
+
easing: options.transitionEasing,
|
|
2923
|
+
});
|
|
2924
|
+
} else {
|
|
2925
|
+
camera.lookAt(target, distance);
|
|
2926
|
+
pubSub.subscribe('draw', resolve, 1);
|
|
2927
|
+
draw = true;
|
|
2928
|
+
}
|
|
2929
|
+
});
|
|
2930
|
+
|
|
2931
|
+
/**
|
|
2932
|
+
* Zoom to the origin
|
|
2933
|
+
* @param {import('./types').ScatterplotMethodOptions['zoomToLocation']} options
|
|
2934
|
+
* @returns {Promise<void>}
|
|
2935
|
+
*/
|
|
2936
|
+
const zoomToOrigin = (options = {}) => zoomToLocation([0, 0], 1, options);
|
|
2937
|
+
|
|
2938
|
+
/**
|
|
2939
|
+
* Get the screen position of a point
|
|
2940
|
+
* @param {number} pointIdx - Point index
|
|
2941
|
+
* @returns {[number, number] | undefined}
|
|
2942
|
+
*/
|
|
2943
|
+
const getScreenPosition = (pointIdx) => {
|
|
2944
|
+
if (!isPointsDrawn) {
|
|
2945
|
+
throw new Error(ERROR_POINTS_NOT_DRAWN);
|
|
2946
|
+
}
|
|
2947
|
+
|
|
2948
|
+
const point = points[pointIdx];
|
|
2949
|
+
|
|
2950
|
+
if (!point) {
|
|
2951
|
+
return undefined;
|
|
2952
|
+
}
|
|
2953
|
+
|
|
2954
|
+
// Homogeneous coordinates of the point
|
|
2955
|
+
const v = [point[0], point[1], 0, 1];
|
|
2956
|
+
|
|
2957
|
+
// Convert to clip space
|
|
2958
|
+
mat4.multiply(
|
|
2959
|
+
scratch,
|
|
2960
|
+
projectionLocal,
|
|
2961
|
+
mat4.multiply(scratch, camera.view, model),
|
|
2962
|
+
);
|
|
2963
|
+
|
|
2964
|
+
vec4.transformMat4(v, v, scratch);
|
|
2965
|
+
|
|
2966
|
+
// Finally, we convert to the screen space
|
|
2967
|
+
const x = (currentWidth * (v[0] + 1)) / 2;
|
|
2968
|
+
const y = currentHeight * (0.5 - v[1] / 2);
|
|
2969
|
+
|
|
2970
|
+
return [x, y];
|
|
2971
|
+
};
|
|
2972
|
+
|
|
2973
|
+
const updatePointConnectionStyle = () => {
|
|
2974
|
+
pointConnections.setStyle({
|
|
2975
|
+
color: getColors(
|
|
2976
|
+
pointConnectionColor,
|
|
2977
|
+
pointConnectionColorActive,
|
|
2978
|
+
pointConnectionColorHover,
|
|
2979
|
+
),
|
|
2980
|
+
opacity:
|
|
2981
|
+
pointConnectionOpacity === null ? null : pointConnectionOpacity[0],
|
|
2982
|
+
width: pointConnectionSize[0],
|
|
2983
|
+
});
|
|
2984
|
+
};
|
|
2985
|
+
|
|
2986
|
+
const updateLassoInitiatorStyle = () => {
|
|
2987
|
+
const v = Math.round(backgroundColorBrightness) > 0.5 ? 0 : 255;
|
|
2988
|
+
lassoManager.initiator.style.border = `1px dashed rgba(${v}, ${v}, ${v}, 0.33)`;
|
|
2989
|
+
lassoManager.initiator.style.background = `rgba(${v}, ${v}, ${v}, 0.1)`;
|
|
2990
|
+
};
|
|
2991
|
+
|
|
2992
|
+
const updateLassoLongPressIndicatorStyle = () => {
|
|
2993
|
+
const v = Math.round(backgroundColorBrightness) > 0.5 ? 0 : 255;
|
|
2994
|
+
|
|
2995
|
+
lassoManager.longPressIndicator.style.color = `rgb(${v}, ${v}, ${v})`;
|
|
2996
|
+
lassoManager.longPressIndicator.dataset.color = `rgb(${v}, ${v}, ${v})`;
|
|
2997
|
+
|
|
2998
|
+
const rgb = lassoColor.map((c) => Math.round(c * 255));
|
|
2999
|
+
lassoManager.longPressIndicator.dataset.activeColor = `rgb(${rgb[0]}, ${rgb[1]}, ${rgb[2]})`;
|
|
3000
|
+
};
|
|
3001
|
+
|
|
3002
|
+
const setBackgroundColor = (newBackgroundColor) => {
|
|
3003
|
+
if (!newBackgroundColor) {
|
|
3004
|
+
return;
|
|
3005
|
+
}
|
|
3006
|
+
|
|
3007
|
+
backgroundColor = toRgba(newBackgroundColor, true);
|
|
3008
|
+
backgroundColorBrightness = rgbBrightness(backgroundColor);
|
|
3009
|
+
updateLassoInitiatorStyle();
|
|
3010
|
+
updateLassoLongPressIndicatorStyle();
|
|
3011
|
+
};
|
|
3012
|
+
|
|
3013
|
+
const setBackgroundImage = (newBackgroundImage) => {
|
|
3014
|
+
if (!newBackgroundImage) {
|
|
3015
|
+
backgroundImage = null;
|
|
3016
|
+
} else if (isString(newBackgroundImage)) {
|
|
3017
|
+
createTextureFromUrl(renderer.regl, newBackgroundImage)
|
|
3018
|
+
.then((texture) => {
|
|
3019
|
+
backgroundImage = texture;
|
|
3020
|
+
draw = true;
|
|
3021
|
+
pubSub.publish('backgroundImageReady');
|
|
3022
|
+
})
|
|
3023
|
+
.catch(() => {
|
|
3024
|
+
// biome-ignore lint/suspicious/noConsole: This is a legitimately useful error log
|
|
3025
|
+
console.error(`Count not create texture from ${newBackgroundImage}`);
|
|
3026
|
+
backgroundImage = null;
|
|
3027
|
+
});
|
|
3028
|
+
} else if (newBackgroundImage._reglType === 'texture2d') {
|
|
3029
|
+
backgroundImage = newBackgroundImage;
|
|
3030
|
+
} else {
|
|
3031
|
+
backgroundImage = null;
|
|
3032
|
+
}
|
|
3033
|
+
};
|
|
3034
|
+
|
|
3035
|
+
const setCameraDistance = (distance) => {
|
|
3036
|
+
if (distance > 0) {
|
|
3037
|
+
camera.lookAt(camera.target, distance, camera.rotation);
|
|
3038
|
+
}
|
|
3039
|
+
};
|
|
3040
|
+
|
|
3041
|
+
const setCameraRotation = (rotation) => {
|
|
3042
|
+
if (rotation !== null) {
|
|
3043
|
+
camera.lookAt(camera.target, camera.distance[0], rotation);
|
|
3044
|
+
}
|
|
3045
|
+
};
|
|
3046
|
+
|
|
3047
|
+
const setCameraTarget = (target) => {
|
|
3048
|
+
if (target) {
|
|
3049
|
+
camera.lookAt(target, camera.distance[0], camera.rotation);
|
|
3050
|
+
}
|
|
3051
|
+
};
|
|
3052
|
+
|
|
3053
|
+
const setCameraView = (view) => {
|
|
3054
|
+
if (view) {
|
|
3055
|
+
camera.setView(view);
|
|
3056
|
+
}
|
|
3057
|
+
};
|
|
3058
|
+
|
|
3059
|
+
const setCameraIsFixed = (isFixed) => {
|
|
3060
|
+
cameraIsFixed = Boolean(isFixed);
|
|
3061
|
+
camera.config({ isFixed: cameraIsFixed });
|
|
3062
|
+
};
|
|
3063
|
+
|
|
3064
|
+
const setLassoColor = (newLassoColor) => {
|
|
3065
|
+
if (!newLassoColor) {
|
|
3066
|
+
return;
|
|
3067
|
+
}
|
|
3068
|
+
|
|
3069
|
+
lassoColor = toRgba(newLassoColor, true);
|
|
3070
|
+
|
|
3071
|
+
lasso.setStyle({ color: lassoColor });
|
|
3072
|
+
|
|
3073
|
+
const rgb = lassoColor.map((c) => Math.round(c * 255));
|
|
3074
|
+
lassoManager.longPressIndicator.dataset.activeColor = `rgb(${rgb[0]}, ${rgb[1]}, ${rgb[2]})`;
|
|
3075
|
+
};
|
|
3076
|
+
|
|
3077
|
+
const setLassoLineWidth = (newLassoLineWidth) => {
|
|
3078
|
+
if (Number.isNaN(+newLassoLineWidth) || +newLassoLineWidth < 1) {
|
|
3079
|
+
return;
|
|
3080
|
+
}
|
|
3081
|
+
|
|
3082
|
+
lassoLineWidth = +newLassoLineWidth;
|
|
3083
|
+
|
|
3084
|
+
lasso.setStyle({ width: lassoLineWidth });
|
|
3085
|
+
};
|
|
3086
|
+
|
|
3087
|
+
const setLassoMinDelay = (newLassoMinDelay) => {
|
|
3088
|
+
if (!+newLassoMinDelay) {
|
|
3089
|
+
return;
|
|
3090
|
+
}
|
|
3091
|
+
|
|
3092
|
+
lassoMinDelay = +newLassoMinDelay;
|
|
3093
|
+
|
|
3094
|
+
lassoManager.set({
|
|
3095
|
+
minDelay: lassoMinDelay,
|
|
3096
|
+
});
|
|
3097
|
+
};
|
|
3098
|
+
|
|
3099
|
+
const setLassoMinDist = (newLassoMinDist) => {
|
|
3100
|
+
if (!+newLassoMinDist) {
|
|
3101
|
+
return;
|
|
3102
|
+
}
|
|
3103
|
+
|
|
3104
|
+
lassoMinDist = +newLassoMinDist;
|
|
3105
|
+
|
|
3106
|
+
lassoManager.set({
|
|
3107
|
+
minDist: lassoMinDist,
|
|
3108
|
+
});
|
|
3109
|
+
};
|
|
3110
|
+
|
|
3111
|
+
const setLassoClearEvent = (newLassoClearEvent) => {
|
|
3112
|
+
lassoClearEvent = limit(
|
|
3113
|
+
LASSO_CLEAR_EVENTS,
|
|
3114
|
+
lassoClearEvent,
|
|
3115
|
+
)(newLassoClearEvent);
|
|
3116
|
+
};
|
|
3117
|
+
|
|
3118
|
+
const setLassoInitiator = (newLassoInitiator) => {
|
|
3119
|
+
lassoInitiator = Boolean(newLassoInitiator);
|
|
3120
|
+
|
|
3121
|
+
lassoManager.set({
|
|
3122
|
+
enableInitiator: lassoInitiator,
|
|
3123
|
+
});
|
|
3124
|
+
};
|
|
3125
|
+
|
|
3126
|
+
const setLassoInitiatorParentElement = (newLassoInitiatorParentElement) => {
|
|
3127
|
+
lassoInitiatorParentElement = newLassoInitiatorParentElement;
|
|
3128
|
+
|
|
3129
|
+
lassoManager.set({
|
|
3130
|
+
initiatorParentElement: lassoInitiatorParentElement,
|
|
3131
|
+
});
|
|
3132
|
+
};
|
|
3133
|
+
|
|
3134
|
+
const setLassoLongPressIndicatorParentElement = (newParentElement) => {
|
|
3135
|
+
lassoLongPressIndicatorParentElement = newParentElement;
|
|
3136
|
+
|
|
3137
|
+
lassoManager.set({
|
|
3138
|
+
longPressIndicatorParentElement: lassoLongPressIndicatorParentElement,
|
|
3139
|
+
});
|
|
3140
|
+
};
|
|
3141
|
+
|
|
3142
|
+
const setLassoOnLongPress = (newLassoOnLongPress) => {
|
|
3143
|
+
lassoOnLongPress = Boolean(newLassoOnLongPress);
|
|
3144
|
+
};
|
|
3145
|
+
|
|
3146
|
+
const setLassoLongPressTime = (newLassoOnLongPressTime) => {
|
|
3147
|
+
lassoLongPressTime = Number(newLassoOnLongPressTime);
|
|
3148
|
+
};
|
|
3149
|
+
|
|
3150
|
+
const setLassoLongPressAfterEffectTime = (newTime) => {
|
|
3151
|
+
lassoLongPressAfterEffectTime = Number(newTime);
|
|
3152
|
+
};
|
|
3153
|
+
|
|
3154
|
+
const setLassoLongPressEffectDelay = (newDelay) => {
|
|
3155
|
+
lassoLongPressEffectDelay = Number(newDelay);
|
|
3156
|
+
};
|
|
3157
|
+
|
|
3158
|
+
const setLassoLongPressRevertEffectTime = (newTime) => {
|
|
3159
|
+
lassoLongPressRevertEffectTime = Number(newTime);
|
|
3160
|
+
};
|
|
3161
|
+
|
|
3162
|
+
const setLassoType = (newType) => {
|
|
3163
|
+
if (newType === 'brush') {
|
|
3164
|
+
lassoManager.set({
|
|
3165
|
+
type: newType,
|
|
3166
|
+
minDist: Math.max(LASSO_BRUSH_MIN_MIN_DIST, lassoMinDist),
|
|
3167
|
+
});
|
|
3168
|
+
} else {
|
|
3169
|
+
lassoManager.set({
|
|
3170
|
+
type: newType,
|
|
3171
|
+
minDist: lassoMinDist,
|
|
3172
|
+
});
|
|
3173
|
+
}
|
|
3174
|
+
lassoType = lassoManager.get('type');
|
|
3175
|
+
};
|
|
3176
|
+
|
|
3177
|
+
const setLassoMode = (newMode) => {
|
|
3178
|
+
lassoMode = newMode;
|
|
3179
|
+
};
|
|
3180
|
+
|
|
3181
|
+
const setLassoBrushSize = (newBrushSize) => {
|
|
3182
|
+
lassoBrushSize = Number(newBrushSize) || lassoBrushSize;
|
|
3183
|
+
lassoManager.set({ brushSize: lassoBrushSize });
|
|
3184
|
+
};
|
|
3185
|
+
|
|
3186
|
+
const updateActionKeyMapChange = () => {
|
|
3187
|
+
if (actionKeyMap[KEY_ACTION_ROTATE]) {
|
|
3188
|
+
camera.config({
|
|
3189
|
+
isRotate: true,
|
|
3190
|
+
mouseDownMoveModKey: actionKeyMap[KEY_ACTION_ROTATE],
|
|
3191
|
+
});
|
|
3192
|
+
} else {
|
|
3193
|
+
camera.config({
|
|
3194
|
+
isRotate: false,
|
|
3195
|
+
});
|
|
3196
|
+
}
|
|
3197
|
+
};
|
|
3198
|
+
|
|
3199
|
+
const setActionKeyMap = (newActionKeyMap) => {
|
|
3200
|
+
actionKeyMap = Object.entries(newActionKeyMap).reduce(
|
|
3201
|
+
(map, [action, key]) => {
|
|
3202
|
+
if (KEYS.includes(key) && KEY_ACTIONS.includes(action)) {
|
|
3203
|
+
map[action] = key;
|
|
3204
|
+
}
|
|
3205
|
+
return map;
|
|
3206
|
+
},
|
|
3207
|
+
{},
|
|
3208
|
+
);
|
|
3209
|
+
|
|
3210
|
+
updateActionKeyMapChange();
|
|
3211
|
+
};
|
|
3212
|
+
|
|
3213
|
+
const setMouseMode = (newMouseMode) => {
|
|
3214
|
+
mouseMode = limit(MOUSE_MODES, MOUSE_MODE_PANZOOM)(newMouseMode);
|
|
3215
|
+
|
|
3216
|
+
camera.config({
|
|
3217
|
+
defaultMouseDownMoveAction:
|
|
3218
|
+
mouseMode === MOUSE_MODE_ROTATE ? 'rotate' : 'pan',
|
|
3219
|
+
});
|
|
3220
|
+
};
|
|
3221
|
+
|
|
3222
|
+
const setShowReticle = (newShowReticle) => {
|
|
3223
|
+
if (newShowReticle === null) {
|
|
3224
|
+
return;
|
|
3225
|
+
}
|
|
3226
|
+
|
|
3227
|
+
showReticle = newShowReticle;
|
|
3228
|
+
};
|
|
3229
|
+
|
|
3230
|
+
const setReticleColor = (newReticleColor) => {
|
|
3231
|
+
if (!newReticleColor) {
|
|
3232
|
+
return;
|
|
3233
|
+
}
|
|
3234
|
+
|
|
3235
|
+
reticleColor = toRgba(newReticleColor, true);
|
|
3236
|
+
|
|
3237
|
+
reticleHLine.setStyle({ color: reticleColor });
|
|
3238
|
+
reticleVLine.setStyle({ color: reticleColor });
|
|
3239
|
+
};
|
|
3240
|
+
|
|
3241
|
+
// biome-ignore lint/style/useNamingConvention: XScale are two words
|
|
3242
|
+
const setXScale = (newXScale) => {
|
|
3243
|
+
if (!newXScale) {
|
|
3244
|
+
return;
|
|
3245
|
+
}
|
|
3246
|
+
|
|
3247
|
+
xScale = newXScale;
|
|
3248
|
+
xDomainStart = newXScale.domain()[0];
|
|
3249
|
+
xDomainSize = newXScale ? newXScale.domain()[1] - newXScale.domain()[0] : 0;
|
|
3250
|
+
xScale.range([0, currentWidth]);
|
|
3251
|
+
updateScales();
|
|
3252
|
+
};
|
|
3253
|
+
|
|
3254
|
+
// biome-ignore lint/style/useNamingConvention: YScale are two words
|
|
3255
|
+
const setYScale = (newYScale) => {
|
|
3256
|
+
if (!newYScale) {
|
|
3257
|
+
return;
|
|
3258
|
+
}
|
|
3259
|
+
|
|
3260
|
+
yScale = newYScale;
|
|
3261
|
+
yDomainStart = yScale.domain()[0];
|
|
3262
|
+
yDomainSize = yScale ? yScale.domain()[1] - yScale.domain()[0] : 0;
|
|
3263
|
+
yScale.range([currentHeight, 0]);
|
|
3264
|
+
updateScales();
|
|
3265
|
+
};
|
|
3266
|
+
|
|
3267
|
+
const setDeselectOnDblClick = (newDeselectOnDblClick) => {
|
|
3268
|
+
deselectOnDblClick = !!newDeselectOnDblClick;
|
|
3269
|
+
};
|
|
3270
|
+
|
|
3271
|
+
const setDeselectOnEscape = (newDeselectOnEscape) => {
|
|
3272
|
+
deselectOnEscape = !!newDeselectOnEscape;
|
|
3273
|
+
};
|
|
3274
|
+
|
|
3275
|
+
const setShowPointConnections = (newShowPointConnections) => {
|
|
3276
|
+
showPointConnections = !!newShowPointConnections;
|
|
3277
|
+
if (showPointConnections) {
|
|
3278
|
+
if (isPointsDrawn && hasPointConnections(points[0])) {
|
|
3279
|
+
setPointConnections(getPoints()).then(() => {
|
|
3280
|
+
pubSub.publish('pointConnectionsDraw');
|
|
3281
|
+
draw = true;
|
|
3282
|
+
});
|
|
3283
|
+
}
|
|
3284
|
+
} else {
|
|
3285
|
+
setPointConnections();
|
|
3286
|
+
}
|
|
3287
|
+
};
|
|
3288
|
+
|
|
3289
|
+
const setPointConnectionColors = (setter, getInheritance) => (newColors) => {
|
|
3290
|
+
if (newColors === 'inherit') {
|
|
3291
|
+
setter([...getInheritance()]);
|
|
3292
|
+
} else {
|
|
3293
|
+
const tmpColors = isMultipleColors(newColors) ? newColors : [newColors];
|
|
3294
|
+
setter(tmpColors.map((color) => toRgba(color, true)));
|
|
3295
|
+
}
|
|
3296
|
+
updatePointConnectionStyle();
|
|
3297
|
+
};
|
|
3298
|
+
|
|
3299
|
+
const setPointConnectionColor = setPointConnectionColors(
|
|
3300
|
+
(newColors) => {
|
|
3301
|
+
pointConnectionColor = newColors;
|
|
3302
|
+
},
|
|
3303
|
+
() => pointColor,
|
|
3304
|
+
);
|
|
3305
|
+
|
|
3306
|
+
const setPointConnectionColorActive = setPointConnectionColors(
|
|
3307
|
+
(newColors) => {
|
|
3308
|
+
pointConnectionColorActive = newColors;
|
|
3309
|
+
},
|
|
3310
|
+
() => pointColorActive,
|
|
3311
|
+
);
|
|
3312
|
+
|
|
3313
|
+
const setPointConnectionColorHover = setPointConnectionColors(
|
|
3314
|
+
(newColors) => {
|
|
3315
|
+
pointConnectionColorHover = newColors;
|
|
3316
|
+
},
|
|
3317
|
+
() => pointColorHover,
|
|
3318
|
+
);
|
|
3319
|
+
|
|
3320
|
+
const setPointConnectionOpacity = (newOpacity) => {
|
|
3321
|
+
if (isConditionalArray(newOpacity, isPositiveNumber, { minLength: 1 })) {
|
|
3322
|
+
pointConnectionOpacity = [...newOpacity];
|
|
3323
|
+
}
|
|
3324
|
+
|
|
3325
|
+
if (isStrictlyPositiveNumber(+newOpacity)) {
|
|
3326
|
+
pointConnectionOpacity = [+newOpacity];
|
|
3327
|
+
}
|
|
3328
|
+
|
|
3329
|
+
pointConnectionColor = pointConnectionColor.map((color) => {
|
|
3330
|
+
color[3] = Number.isNaN(+pointConnectionOpacity[0])
|
|
3331
|
+
? color[3]
|
|
3332
|
+
: +pointConnectionOpacity[0];
|
|
3333
|
+
return color;
|
|
3334
|
+
});
|
|
3335
|
+
|
|
3336
|
+
updatePointConnectionStyle();
|
|
3337
|
+
};
|
|
3338
|
+
|
|
3339
|
+
const setPointConnectionOpacityActive = (newOpacity) => {
|
|
3340
|
+
if (!Number.isNaN(+newOpacity) && +newOpacity) {
|
|
3341
|
+
pointConnectionOpacityActive = +newOpacity;
|
|
3342
|
+
}
|
|
3343
|
+
};
|
|
3344
|
+
|
|
3345
|
+
const setPointConnectionSize = (newPointConnectionSize) => {
|
|
3346
|
+
if (
|
|
3347
|
+
isConditionalArray(newPointConnectionSize, isPositiveNumber, {
|
|
3348
|
+
minLength: 1,
|
|
3349
|
+
})
|
|
3350
|
+
) {
|
|
3351
|
+
pointConnectionSize = [...newPointConnectionSize];
|
|
3352
|
+
}
|
|
3353
|
+
|
|
3354
|
+
if (isStrictlyPositiveNumber(+newPointConnectionSize)) {
|
|
3355
|
+
pointConnectionSize = [+newPointConnectionSize];
|
|
3356
|
+
}
|
|
3357
|
+
|
|
3358
|
+
updatePointConnectionStyle();
|
|
3359
|
+
};
|
|
3360
|
+
|
|
3361
|
+
const setPointConnectionSizeActive = (newPointConnectionSizeActive) => {
|
|
3362
|
+
if (
|
|
3363
|
+
!Number.isNaN(+newPointConnectionSizeActive) &&
|
|
3364
|
+
+newPointConnectionSizeActive
|
|
3365
|
+
) {
|
|
3366
|
+
pointConnectionSizeActive = Math.max(0, newPointConnectionSizeActive);
|
|
3367
|
+
}
|
|
3368
|
+
};
|
|
3369
|
+
|
|
3370
|
+
const setPointConnectionMaxIntPointsPerSegment = (
|
|
3371
|
+
newPointConnectionMaxIntPointsPerSegment,
|
|
3372
|
+
) => {
|
|
3373
|
+
pointConnectionMaxIntPointsPerSegment = Math.max(
|
|
3374
|
+
0,
|
|
3375
|
+
newPointConnectionMaxIntPointsPerSegment,
|
|
3376
|
+
);
|
|
3377
|
+
};
|
|
3378
|
+
|
|
3379
|
+
const setPointConnectionTolerance = (newPointConnectionTolerance) => {
|
|
3380
|
+
pointConnectionTolerance = Math.max(0, newPointConnectionTolerance);
|
|
3381
|
+
};
|
|
3382
|
+
|
|
3383
|
+
const setPointSizeMouseDetection = (newPointSizeMouseDetection) => {
|
|
3384
|
+
pointSizeMouseDetection = newPointSizeMouseDetection;
|
|
3385
|
+
computePointSizeMouseDetection();
|
|
3386
|
+
};
|
|
3387
|
+
|
|
3388
|
+
const setPointScaleMode = (newPointScaleMode) => {
|
|
3389
|
+
switch (newPointScaleMode) {
|
|
3390
|
+
case 'linear': {
|
|
3391
|
+
pointScaleMode = newPointScaleMode;
|
|
3392
|
+
getPointScale = getLinearPointScale;
|
|
3393
|
+
break;
|
|
3394
|
+
}
|
|
3395
|
+
case 'constant': {
|
|
3396
|
+
pointScaleMode = newPointScaleMode;
|
|
3397
|
+
getPointScale = getConstantPointScale;
|
|
3398
|
+
break;
|
|
3399
|
+
}
|
|
3400
|
+
default: {
|
|
3401
|
+
pointScaleMode = 'asinh';
|
|
3402
|
+
getPointScale = getAsinhPointScale;
|
|
3403
|
+
break;
|
|
3404
|
+
}
|
|
3405
|
+
}
|
|
3406
|
+
};
|
|
3407
|
+
|
|
3408
|
+
const setOpacityByDensityFill = (newOpacityByDensityFill) => {
|
|
3409
|
+
opacityByDensityFill = +newOpacityByDensityFill;
|
|
3410
|
+
};
|
|
3411
|
+
|
|
3412
|
+
const setOpacityInactiveMax = (newOpacityInactiveMax) => {
|
|
3413
|
+
opacityInactiveMax = +newOpacityInactiveMax;
|
|
3414
|
+
};
|
|
3415
|
+
|
|
3416
|
+
const setOpacityInactiveScale = (newOpacityInactiveScale) => {
|
|
3417
|
+
opacityInactiveScale = +newOpacityInactiveScale;
|
|
3418
|
+
};
|
|
3419
|
+
|
|
3420
|
+
const setAnnotationLineColor = (newAnnotationLineColor) => {
|
|
3421
|
+
annotationLineColor = toRgba(newAnnotationLineColor);
|
|
3422
|
+
};
|
|
3423
|
+
|
|
3424
|
+
const setAnnotationLineWidth = (newAnnotationLineWidth) => {
|
|
3425
|
+
annotationLineWidth = +newAnnotationLineWidth;
|
|
3426
|
+
};
|
|
3427
|
+
|
|
3428
|
+
// biome-ignore lint/style/useNamingConvention: HVLine stands for horizontal vertical line
|
|
3429
|
+
const setAnnotationHVLineLimit = (newAnnotationHVLineLimit) => {
|
|
3430
|
+
annotationHVLineLimit = +newAnnotationHVLineLimit;
|
|
3431
|
+
};
|
|
3432
|
+
|
|
3433
|
+
const setGamma = (newGamma) => {
|
|
3434
|
+
renderer.gamma = newGamma;
|
|
3435
|
+
};
|
|
3436
|
+
|
|
3437
|
+
const setAntiAliasing = (newAntiAliasing) => {
|
|
3438
|
+
antiAliasing = Number(newAntiAliasing) || 0.5;
|
|
3439
|
+
};
|
|
3440
|
+
|
|
3441
|
+
const setPixelAligned = (newPixelAligned) => {
|
|
3442
|
+
pixelAligned = Boolean(newPixelAligned);
|
|
3443
|
+
};
|
|
3444
|
+
|
|
3445
|
+
/** @type {<Key extends keyof import('./types').Properties>(property: Key) => import('./types').Properties[Key] } */
|
|
3446
|
+
const get = (prop) => {
|
|
3447
|
+
const [property] = Object.keys(
|
|
3448
|
+
checkDeprecations({
|
|
3449
|
+
[prop]: SKIP_DEPRECATION_VALUE_TRANSLATION,
|
|
3450
|
+
}),
|
|
3451
|
+
);
|
|
3452
|
+
|
|
3453
|
+
if (property === 'aspectRatio') {
|
|
3454
|
+
return dataAspectRatio;
|
|
3455
|
+
}
|
|
3456
|
+
|
|
3457
|
+
if (property === 'background') {
|
|
3458
|
+
return backgroundColor;
|
|
3459
|
+
}
|
|
3460
|
+
|
|
3461
|
+
if (property === 'backgroundColor') {
|
|
3462
|
+
return backgroundColor;
|
|
3463
|
+
}
|
|
3464
|
+
|
|
3465
|
+
if (property === 'backgroundImage') {
|
|
3466
|
+
return backgroundImage;
|
|
3467
|
+
}
|
|
3468
|
+
|
|
3469
|
+
if (property === 'camera') {
|
|
3470
|
+
return camera;
|
|
3471
|
+
}
|
|
3472
|
+
|
|
3473
|
+
if (property === 'cameraTarget') {
|
|
3474
|
+
return camera.target;
|
|
3475
|
+
}
|
|
3476
|
+
|
|
3477
|
+
if (property === 'cameraDistance') {
|
|
3478
|
+
return camera.distance[0];
|
|
3479
|
+
}
|
|
3480
|
+
|
|
3481
|
+
if (property === 'cameraRotation') {
|
|
3482
|
+
return camera.rotation;
|
|
3483
|
+
}
|
|
3484
|
+
|
|
3485
|
+
if (property === 'cameraView') {
|
|
3486
|
+
return camera.view;
|
|
3487
|
+
}
|
|
3488
|
+
|
|
3489
|
+
if (property === 'cameraIsFixed') {
|
|
3490
|
+
return cameraIsFixed;
|
|
3491
|
+
}
|
|
3492
|
+
|
|
3493
|
+
if (property === 'canvas') {
|
|
3494
|
+
return canvas;
|
|
3495
|
+
}
|
|
3496
|
+
|
|
3497
|
+
if (property === 'colorBy') {
|
|
3498
|
+
return colorBy;
|
|
3499
|
+
}
|
|
3500
|
+
|
|
3501
|
+
if (property === 'sizeBy') {
|
|
3502
|
+
return sizeBy;
|
|
3503
|
+
}
|
|
3504
|
+
|
|
3505
|
+
if (property === 'deselectOnDblClick') {
|
|
3506
|
+
return deselectOnDblClick;
|
|
3507
|
+
}
|
|
3508
|
+
|
|
3509
|
+
if (property === 'deselectOnEscape') {
|
|
3510
|
+
return deselectOnEscape;
|
|
3511
|
+
}
|
|
3512
|
+
|
|
3513
|
+
if (property === 'height') {
|
|
3514
|
+
return height;
|
|
3515
|
+
}
|
|
3516
|
+
|
|
3517
|
+
if (property === 'lassoColor') {
|
|
3518
|
+
return lassoColor;
|
|
3519
|
+
}
|
|
3520
|
+
|
|
3521
|
+
if (property === 'lassoLineWidth') {
|
|
3522
|
+
return lassoLineWidth;
|
|
3523
|
+
}
|
|
3524
|
+
|
|
3525
|
+
if (property === 'lassoMinDelay') {
|
|
3526
|
+
return lassoMinDelay;
|
|
3527
|
+
}
|
|
3528
|
+
|
|
3529
|
+
if (property === 'lassoMinDist') {
|
|
3530
|
+
return lassoMinDist;
|
|
3531
|
+
}
|
|
3532
|
+
|
|
3533
|
+
if (property === 'lassoClearEvent') {
|
|
3534
|
+
return lassoClearEvent;
|
|
3535
|
+
}
|
|
3536
|
+
|
|
3537
|
+
if (property === 'lassoInitiator') {
|
|
3538
|
+
return lassoInitiator;
|
|
3539
|
+
}
|
|
3540
|
+
|
|
3541
|
+
if (property === 'lassoInitiatorElement') {
|
|
3542
|
+
return lassoManager.initiator;
|
|
3543
|
+
}
|
|
3544
|
+
|
|
3545
|
+
if (property === 'lassoInitiatorParentElement') {
|
|
3546
|
+
return lassoInitiatorParentElement;
|
|
3547
|
+
}
|
|
3548
|
+
|
|
3549
|
+
if (property === 'lassoLongPressIndicatorParentElement') {
|
|
3550
|
+
return lassoLongPressIndicatorParentElement;
|
|
3551
|
+
}
|
|
3552
|
+
|
|
3553
|
+
if (property === 'lassoOnLongPress') {
|
|
3554
|
+
return lassoOnLongPress;
|
|
3555
|
+
}
|
|
3556
|
+
|
|
3557
|
+
if (property === 'lassoType') {
|
|
3558
|
+
return lassoType;
|
|
3559
|
+
}
|
|
3560
|
+
|
|
3561
|
+
if (property === 'lassoBrushSize') {
|
|
3562
|
+
return lassoBrushSize;
|
|
3563
|
+
}
|
|
3564
|
+
|
|
3565
|
+
if (property === 'mouseMode') {
|
|
3566
|
+
return mouseMode;
|
|
3567
|
+
}
|
|
3568
|
+
|
|
3569
|
+
if (property === 'opacity') {
|
|
3570
|
+
return opacity.length === 1 ? opacity[0] : opacity;
|
|
3571
|
+
}
|
|
3572
|
+
|
|
3573
|
+
if (property === 'opacityBy') {
|
|
3574
|
+
return opacityBy;
|
|
3575
|
+
}
|
|
3576
|
+
|
|
3577
|
+
if (property === 'opacityByDensityFill') {
|
|
3578
|
+
return opacityByDensityFill;
|
|
3579
|
+
}
|
|
3580
|
+
|
|
3581
|
+
if (property === 'opacityByDensityDebounceTime') {
|
|
3582
|
+
return opacityByDensityDebounceTime;
|
|
3583
|
+
}
|
|
3584
|
+
|
|
3585
|
+
if (property === 'opacityInactiveMax') {
|
|
3586
|
+
return opacityInactiveMax;
|
|
3587
|
+
}
|
|
3588
|
+
|
|
3589
|
+
if (property === 'opacityInactiveScale') {
|
|
3590
|
+
return opacityInactiveScale;
|
|
3591
|
+
}
|
|
3592
|
+
|
|
3593
|
+
if (property === 'points') {
|
|
3594
|
+
return points;
|
|
3595
|
+
}
|
|
3596
|
+
|
|
3597
|
+
if (property === 'hoveredPoint') {
|
|
3598
|
+
return hoveredPoint;
|
|
3599
|
+
}
|
|
3600
|
+
|
|
3601
|
+
if (property === 'selectedPoints') {
|
|
3602
|
+
return [...selectedPoints];
|
|
3603
|
+
}
|
|
3604
|
+
|
|
3605
|
+
if (property === 'filteredPoints') {
|
|
3606
|
+
return isPointsFiltered
|
|
3607
|
+
? Array.from(filteredPointsSet)
|
|
3608
|
+
: Array.from({ length: points.length }, (_, i) => i);
|
|
3609
|
+
}
|
|
3610
|
+
|
|
3611
|
+
if (property === 'pointsInView') {
|
|
3612
|
+
return getPointsInView();
|
|
3613
|
+
}
|
|
3614
|
+
|
|
3615
|
+
if (property === 'pointColor') {
|
|
3616
|
+
return pointColor.length === 1 ? pointColor[0] : pointColor;
|
|
3617
|
+
}
|
|
3618
|
+
|
|
3619
|
+
if (property === 'pointColorActive') {
|
|
3620
|
+
return pointColorActive.length === 1
|
|
3621
|
+
? pointColorActive[0]
|
|
3622
|
+
: pointColorActive;
|
|
3623
|
+
}
|
|
3624
|
+
|
|
3625
|
+
if (property === 'pointColorHover') {
|
|
3626
|
+
return pointColorHover.length === 1
|
|
3627
|
+
? pointColorHover[0]
|
|
3628
|
+
: pointColorHover;
|
|
3629
|
+
}
|
|
3630
|
+
|
|
3631
|
+
if (property === 'pointOutlineWidth') {
|
|
3632
|
+
return pointOutlineWidth;
|
|
3633
|
+
}
|
|
3634
|
+
|
|
3635
|
+
if (property === 'pointSize') {
|
|
3636
|
+
return pointSize.length === 1 ? pointSize[0] : pointSize;
|
|
3637
|
+
}
|
|
3638
|
+
|
|
3639
|
+
if (property === 'pointSizeSelected') {
|
|
3640
|
+
return pointSizeSelected;
|
|
3641
|
+
}
|
|
3642
|
+
|
|
3643
|
+
if (property === 'pointSizeMouseDetection') {
|
|
3644
|
+
return pointSizeMouseDetection;
|
|
3645
|
+
}
|
|
3646
|
+
|
|
3647
|
+
if (property === 'showPointConnections') {
|
|
3648
|
+
return showPointConnections;
|
|
3649
|
+
}
|
|
3650
|
+
|
|
3651
|
+
if (property === 'pointConnectionColor') {
|
|
3652
|
+
return pointConnectionColor.length === 1
|
|
3653
|
+
? pointConnectionColor[0]
|
|
3654
|
+
: pointConnectionColor;
|
|
3655
|
+
}
|
|
3656
|
+
|
|
3657
|
+
if (property === 'pointConnectionColorActive') {
|
|
3658
|
+
return pointConnectionColorActive.length === 1
|
|
3659
|
+
? pointConnectionColorActive[0]
|
|
3660
|
+
: pointConnectionColorActive;
|
|
3661
|
+
}
|
|
3662
|
+
|
|
3663
|
+
if (property === 'pointConnectionColorHover') {
|
|
3664
|
+
return pointConnectionColorHover.length === 1
|
|
3665
|
+
? pointConnectionColorHover[0]
|
|
3666
|
+
: pointConnectionColorHover;
|
|
3667
|
+
}
|
|
3668
|
+
|
|
3669
|
+
if (property === 'pointConnectionColorBy') {
|
|
3670
|
+
return pointConnectionColorBy;
|
|
3671
|
+
}
|
|
3672
|
+
|
|
3673
|
+
if (property === 'pointConnectionOpacity') {
|
|
3674
|
+
return pointConnectionOpacity.length === 1
|
|
3675
|
+
? pointConnectionOpacity[0]
|
|
3676
|
+
: pointConnectionOpacity;
|
|
3677
|
+
}
|
|
3678
|
+
|
|
3679
|
+
if (property === 'pointConnectionOpacityBy') {
|
|
3680
|
+
return pointConnectionOpacityBy;
|
|
3681
|
+
}
|
|
3682
|
+
|
|
3683
|
+
if (property === 'pointConnectionOpacityActive') {
|
|
3684
|
+
return pointConnectionOpacityActive;
|
|
3685
|
+
}
|
|
3686
|
+
|
|
3687
|
+
if (property === 'pointConnectionSize') {
|
|
3688
|
+
return pointConnectionSize.length === 1
|
|
3689
|
+
? pointConnectionSize[0]
|
|
3690
|
+
: pointConnectionSize;
|
|
3691
|
+
}
|
|
3692
|
+
|
|
3693
|
+
if (property === 'pointConnectionSizeActive') {
|
|
3694
|
+
return pointConnectionSizeActive;
|
|
3695
|
+
}
|
|
3696
|
+
|
|
3697
|
+
if (property === 'pointConnectionSizeBy') {
|
|
3698
|
+
return pointConnectionSizeBy;
|
|
3699
|
+
}
|
|
3700
|
+
|
|
3701
|
+
if (property === 'pointConnectionMaxIntPointsPerSegment') {
|
|
3702
|
+
return pointConnectionMaxIntPointsPerSegment;
|
|
3703
|
+
}
|
|
3704
|
+
|
|
3705
|
+
if (property === 'pointConnectionTolerance') {
|
|
3706
|
+
return pointConnectionTolerance;
|
|
3707
|
+
}
|
|
3708
|
+
|
|
3709
|
+
if (property === 'pointScaleMode') {
|
|
3710
|
+
return pointScaleMode;
|
|
3711
|
+
}
|
|
3712
|
+
|
|
3713
|
+
if (property === 'reticleColor') {
|
|
3714
|
+
return reticleColor;
|
|
3715
|
+
}
|
|
3716
|
+
|
|
3717
|
+
if (property === 'regl') {
|
|
3718
|
+
return renderer.regl;
|
|
3719
|
+
}
|
|
3720
|
+
|
|
3721
|
+
if (property === 'showReticle') {
|
|
3722
|
+
return showReticle;
|
|
3723
|
+
}
|
|
3724
|
+
|
|
3725
|
+
if (property === 'version') {
|
|
3726
|
+
return version;
|
|
3727
|
+
}
|
|
3728
|
+
|
|
3729
|
+
if (property === 'width') {
|
|
3730
|
+
return width;
|
|
3731
|
+
}
|
|
3732
|
+
|
|
3733
|
+
if (property === 'xScale') {
|
|
3734
|
+
return xScale;
|
|
3735
|
+
}
|
|
3736
|
+
|
|
3737
|
+
if (property === 'yScale') {
|
|
3738
|
+
return yScale;
|
|
3739
|
+
}
|
|
3740
|
+
|
|
3741
|
+
if (property === 'performanceMode') {
|
|
3742
|
+
return performanceMode;
|
|
3743
|
+
}
|
|
3744
|
+
|
|
3745
|
+
if (property === 'renderPointsAsSquares') {
|
|
3746
|
+
return renderPointsAsSquares;
|
|
3747
|
+
}
|
|
3748
|
+
|
|
3749
|
+
if (property === 'disableAlphaBlending') {
|
|
3750
|
+
return disableAlphaBlending;
|
|
3751
|
+
}
|
|
3752
|
+
|
|
3753
|
+
if (property === 'gamma') {
|
|
3754
|
+
return renderer.gamma;
|
|
3755
|
+
}
|
|
3756
|
+
|
|
3757
|
+
if (property === 'renderer') {
|
|
3758
|
+
return renderer;
|
|
3759
|
+
}
|
|
3760
|
+
|
|
3761
|
+
if (property === 'isDestroyed') {
|
|
3762
|
+
return isDestroyed;
|
|
3763
|
+
}
|
|
3764
|
+
|
|
3765
|
+
if (property === 'isDrawing') {
|
|
3766
|
+
return isDrawing;
|
|
3767
|
+
}
|
|
3768
|
+
|
|
3769
|
+
if (property === 'isPointsDrawn') {
|
|
3770
|
+
return isPointsDrawn;
|
|
3771
|
+
}
|
|
3772
|
+
|
|
3773
|
+
if (property === 'isPointsFiltered') {
|
|
3774
|
+
return isPointsFiltered;
|
|
3775
|
+
}
|
|
3776
|
+
|
|
3777
|
+
if (property === 'isAnnotationsDrawn') {
|
|
3778
|
+
return isAnnotationsDrawn;
|
|
3779
|
+
}
|
|
3780
|
+
|
|
3781
|
+
if (property === 'zDataType') {
|
|
3782
|
+
return valueZDataType;
|
|
3783
|
+
}
|
|
3784
|
+
|
|
3785
|
+
if (property === 'wDataType') {
|
|
3786
|
+
return valueWDataType;
|
|
3787
|
+
}
|
|
3788
|
+
|
|
3789
|
+
if (property === 'spatialIndex') {
|
|
3790
|
+
return spatialIndex?.data;
|
|
3791
|
+
}
|
|
3792
|
+
|
|
3793
|
+
if (property === 'annotationLineColor') {
|
|
3794
|
+
return annotationLineColor;
|
|
3795
|
+
}
|
|
3796
|
+
|
|
3797
|
+
if (property === 'annotationLineWidth') {
|
|
3798
|
+
return annotationLineWidth;
|
|
3799
|
+
}
|
|
3800
|
+
|
|
3801
|
+
if (property === 'annotationHVLineLimit') {
|
|
3802
|
+
return annotationHVLineLimit;
|
|
3803
|
+
}
|
|
3804
|
+
|
|
3805
|
+
if (property === 'antiAliasing') {
|
|
3806
|
+
return antiAliasing;
|
|
3807
|
+
}
|
|
3808
|
+
|
|
3809
|
+
if (property === 'pixelAligned') {
|
|
3810
|
+
return pixelAligned;
|
|
3811
|
+
}
|
|
3812
|
+
|
|
3813
|
+
if (property === 'actionKeyMap') {
|
|
3814
|
+
return { ...actionKeyMap };
|
|
3815
|
+
}
|
|
3816
|
+
|
|
3817
|
+
return undefined;
|
|
3818
|
+
};
|
|
3819
|
+
|
|
3820
|
+
/** @type {(properties: Partial<import('./types').Settable>) => Promise<void>} */
|
|
3821
|
+
const set = (properties = {}) => {
|
|
3822
|
+
if (isDestroyed) {
|
|
3823
|
+
return Promise.reject(new Error(ERROR_INSTANCE_IS_DESTROYED));
|
|
3824
|
+
}
|
|
3825
|
+
|
|
3826
|
+
checkDeprecations(properties);
|
|
3827
|
+
|
|
3828
|
+
if (
|
|
3829
|
+
properties.backgroundColor !== undefined ||
|
|
3830
|
+
properties.background !== undefined
|
|
3831
|
+
) {
|
|
3832
|
+
setBackgroundColor(properties.backgroundColor || properties.background);
|
|
3833
|
+
}
|
|
3834
|
+
|
|
3835
|
+
if (properties.backgroundImage !== undefined) {
|
|
3836
|
+
setBackgroundImage(properties.backgroundImage);
|
|
3837
|
+
}
|
|
3838
|
+
|
|
3839
|
+
if (properties.cameraTarget !== undefined) {
|
|
3840
|
+
setCameraTarget(properties.cameraTarget);
|
|
3841
|
+
}
|
|
3842
|
+
|
|
3843
|
+
if (properties.cameraDistance !== undefined) {
|
|
3844
|
+
setCameraDistance(properties.cameraDistance);
|
|
3845
|
+
}
|
|
3846
|
+
|
|
3847
|
+
if (properties.cameraRotation !== undefined) {
|
|
3848
|
+
setCameraRotation(properties.cameraRotation);
|
|
3849
|
+
}
|
|
3850
|
+
|
|
3851
|
+
if (properties.cameraView !== undefined) {
|
|
3852
|
+
setCameraView(properties.cameraView);
|
|
3853
|
+
}
|
|
3854
|
+
|
|
3855
|
+
if (properties.cameraIsFixed !== undefined) {
|
|
3856
|
+
setCameraIsFixed(properties.cameraIsFixed);
|
|
3857
|
+
}
|
|
3858
|
+
|
|
3859
|
+
if (properties.colorBy !== undefined) {
|
|
3860
|
+
setColorBy(properties.colorBy);
|
|
3861
|
+
}
|
|
3862
|
+
|
|
3863
|
+
if (properties.pointColor !== undefined) {
|
|
3864
|
+
setPointColor(properties.pointColor);
|
|
3865
|
+
}
|
|
3866
|
+
|
|
3867
|
+
if (properties.pointColorActive !== undefined) {
|
|
3868
|
+
setPointColorActive(properties.pointColorActive);
|
|
3869
|
+
}
|
|
3870
|
+
|
|
3871
|
+
if (properties.pointColorHover !== undefined) {
|
|
3872
|
+
setPointColorHover(properties.pointColorHover);
|
|
3873
|
+
}
|
|
3874
|
+
|
|
3875
|
+
if (properties.pointSize !== undefined) {
|
|
3876
|
+
setPointSize(properties.pointSize);
|
|
3877
|
+
}
|
|
3878
|
+
|
|
3879
|
+
if (properties.pointSizeSelected !== undefined) {
|
|
3880
|
+
setPointSizeSelected(properties.pointSizeSelected);
|
|
3881
|
+
}
|
|
3882
|
+
|
|
3883
|
+
if (properties.pointSizeMouseDetection !== undefined) {
|
|
3884
|
+
setPointSizeMouseDetection(properties.pointSizeMouseDetection);
|
|
3885
|
+
}
|
|
3886
|
+
|
|
3887
|
+
if (properties.sizeBy !== undefined) {
|
|
3888
|
+
setSizeBy(properties.sizeBy);
|
|
3889
|
+
}
|
|
3890
|
+
|
|
3891
|
+
if (properties.opacity !== undefined) {
|
|
3892
|
+
setOpacity(properties.opacity);
|
|
3893
|
+
}
|
|
3894
|
+
|
|
3895
|
+
if (properties.showPointConnections !== undefined) {
|
|
3896
|
+
setShowPointConnections(properties.showPointConnections);
|
|
3897
|
+
}
|
|
3898
|
+
|
|
3899
|
+
if (properties.pointConnectionColor !== undefined) {
|
|
3900
|
+
setPointConnectionColor(properties.pointConnectionColor);
|
|
3901
|
+
}
|
|
3902
|
+
|
|
3903
|
+
if (properties.pointConnectionColorActive !== undefined) {
|
|
3904
|
+
setPointConnectionColorActive(properties.pointConnectionColorActive);
|
|
3905
|
+
}
|
|
3906
|
+
|
|
3907
|
+
if (properties.pointConnectionColorHover !== undefined) {
|
|
3908
|
+
setPointConnectionColorHover(properties.pointConnectionColorHover);
|
|
3909
|
+
}
|
|
3910
|
+
|
|
3911
|
+
if (properties.pointConnectionColorBy !== undefined) {
|
|
3912
|
+
setPointConnectionColorBy(properties.pointConnectionColorBy);
|
|
3913
|
+
}
|
|
3914
|
+
|
|
3915
|
+
if (properties.pointConnectionOpacityBy !== undefined) {
|
|
3916
|
+
setPointConnectionOpacityBy(properties.pointConnectionOpacityBy);
|
|
3917
|
+
}
|
|
3918
|
+
|
|
3919
|
+
if (properties.pointConnectionOpacity !== undefined) {
|
|
3920
|
+
setPointConnectionOpacity(properties.pointConnectionOpacity);
|
|
3921
|
+
}
|
|
3922
|
+
|
|
3923
|
+
if (properties.pointConnectionOpacityActive !== undefined) {
|
|
3924
|
+
setPointConnectionOpacityActive(properties.pointConnectionOpacityActive);
|
|
3925
|
+
}
|
|
3926
|
+
|
|
3927
|
+
if (properties.pointConnectionSize !== undefined) {
|
|
3928
|
+
setPointConnectionSize(properties.pointConnectionSize);
|
|
3929
|
+
}
|
|
3930
|
+
|
|
3931
|
+
if (properties.pointConnectionSizeActive !== undefined) {
|
|
3932
|
+
setPointConnectionSizeActive(properties.pointConnectionSizeActive);
|
|
3933
|
+
}
|
|
3934
|
+
|
|
3935
|
+
if (properties.pointConnectionSizeBy !== undefined) {
|
|
3936
|
+
setPointConnectionSizeBy(properties.pointConnectionSizeBy);
|
|
3937
|
+
}
|
|
3938
|
+
|
|
3939
|
+
if (properties.pointConnectionMaxIntPointsPerSegment !== undefined) {
|
|
3940
|
+
setPointConnectionMaxIntPointsPerSegment(
|
|
3941
|
+
properties.pointConnectionMaxIntPointsPerSegment,
|
|
3942
|
+
);
|
|
3943
|
+
}
|
|
3944
|
+
|
|
3945
|
+
if (properties.pointConnectionTolerance !== undefined) {
|
|
3946
|
+
setPointConnectionTolerance(properties.pointConnectionTolerance);
|
|
3947
|
+
}
|
|
3948
|
+
|
|
3949
|
+
if (properties.pointScaleMode !== undefined) {
|
|
3950
|
+
setPointScaleMode(properties.pointScaleMode);
|
|
3951
|
+
}
|
|
3952
|
+
|
|
3953
|
+
if (properties.opacityBy !== undefined) {
|
|
3954
|
+
setOpacityBy(properties.opacityBy);
|
|
3955
|
+
}
|
|
3956
|
+
|
|
3957
|
+
if (properties.lassoColor !== undefined) {
|
|
3958
|
+
setLassoColor(properties.lassoColor);
|
|
3959
|
+
}
|
|
3960
|
+
|
|
3961
|
+
if (properties.lassoLineWidth !== undefined) {
|
|
3962
|
+
setLassoLineWidth(properties.lassoLineWidth);
|
|
3963
|
+
}
|
|
3964
|
+
|
|
3965
|
+
if (properties.lassoMinDelay !== undefined) {
|
|
3966
|
+
setLassoMinDelay(properties.lassoMinDelay);
|
|
3967
|
+
}
|
|
3968
|
+
|
|
3969
|
+
if (properties.lassoMinDist !== undefined) {
|
|
3970
|
+
setLassoMinDist(properties.lassoMinDist);
|
|
3971
|
+
}
|
|
3972
|
+
|
|
3973
|
+
if (properties.lassoClearEvent !== undefined) {
|
|
3974
|
+
setLassoClearEvent(properties.lassoClearEvent);
|
|
3975
|
+
}
|
|
3976
|
+
|
|
3977
|
+
if (properties.lassoInitiator !== undefined) {
|
|
3978
|
+
setLassoInitiator(properties.lassoInitiator);
|
|
3979
|
+
}
|
|
3980
|
+
|
|
3981
|
+
if (properties.lassoInitiatorParentElement !== undefined) {
|
|
3982
|
+
setLassoInitiatorParentElement(properties.lassoInitiatorParentElement);
|
|
3983
|
+
}
|
|
3984
|
+
|
|
3985
|
+
if (properties.lassoLongPressIndicatorParentElement !== undefined) {
|
|
3986
|
+
setLassoLongPressIndicatorParentElement(
|
|
3987
|
+
properties.lassoLongPressIndicatorParentElement,
|
|
3988
|
+
);
|
|
3989
|
+
}
|
|
3990
|
+
|
|
3991
|
+
if (properties.lassoOnLongPress !== undefined) {
|
|
3992
|
+
setLassoOnLongPress(properties.lassoOnLongPress);
|
|
3993
|
+
}
|
|
3994
|
+
|
|
3995
|
+
if (properties.lassoLongPressTime !== undefined) {
|
|
3996
|
+
setLassoLongPressTime(properties.lassoLongPressTime);
|
|
3997
|
+
}
|
|
3998
|
+
|
|
3999
|
+
if (properties.lassoLongPressAfterEffectTime !== undefined) {
|
|
4000
|
+
setLassoLongPressAfterEffectTime(
|
|
4001
|
+
properties.lassoLongPressAfterEffectTime,
|
|
4002
|
+
);
|
|
4003
|
+
}
|
|
4004
|
+
|
|
4005
|
+
if (properties.lassoLongPressEffectDelay !== undefined) {
|
|
4006
|
+
setLassoLongPressEffectDelay(properties.lassoLongPressEffectDelay);
|
|
4007
|
+
}
|
|
4008
|
+
|
|
4009
|
+
if (properties.lassoLongPressRevertEffectTime !== undefined) {
|
|
4010
|
+
setLassoLongPressRevertEffectTime(
|
|
4011
|
+
properties.lassoLongPressRevertEffectTime,
|
|
4012
|
+
);
|
|
4013
|
+
}
|
|
4014
|
+
|
|
4015
|
+
if (properties.lassoType !== undefined) {
|
|
4016
|
+
setLassoType(properties.lassoType);
|
|
4017
|
+
}
|
|
4018
|
+
|
|
4019
|
+
if (properties.lassoMode !== undefined) {
|
|
4020
|
+
setLassoMode(properties.lassoMode);
|
|
4021
|
+
}
|
|
4022
|
+
|
|
4023
|
+
if (properties.lassoBrushSize !== undefined) {
|
|
4024
|
+
setLassoBrushSize(properties.lassoBrushSize);
|
|
4025
|
+
}
|
|
4026
|
+
|
|
4027
|
+
if (properties.actionKeyMap !== undefined) {
|
|
4028
|
+
setActionKeyMap(properties.actionKeyMap);
|
|
4029
|
+
}
|
|
4030
|
+
|
|
4031
|
+
if (properties.mouseMode !== undefined) {
|
|
4032
|
+
setMouseMode(properties.mouseMode);
|
|
4033
|
+
}
|
|
4034
|
+
|
|
4035
|
+
if (properties.showReticle !== undefined) {
|
|
4036
|
+
setShowReticle(properties.showReticle);
|
|
4037
|
+
}
|
|
4038
|
+
|
|
4039
|
+
if (properties.reticleColor !== undefined) {
|
|
4040
|
+
setReticleColor(properties.reticleColor);
|
|
4041
|
+
}
|
|
4042
|
+
|
|
4043
|
+
if (properties.pointOutlineWidth !== undefined) {
|
|
4044
|
+
setPointOutlineWidth(properties.pointOutlineWidth);
|
|
4045
|
+
}
|
|
4046
|
+
|
|
4047
|
+
if (properties.height !== undefined) {
|
|
4048
|
+
setHeight(properties.height);
|
|
4049
|
+
}
|
|
4050
|
+
|
|
4051
|
+
if (properties.width !== undefined) {
|
|
4052
|
+
setWidth(properties.width);
|
|
4053
|
+
}
|
|
4054
|
+
|
|
4055
|
+
if (properties.aspectRatio !== undefined) {
|
|
4056
|
+
setDataAspectRatio(properties.aspectRatio);
|
|
4057
|
+
}
|
|
4058
|
+
|
|
4059
|
+
if (properties.xScale !== undefined) {
|
|
4060
|
+
setXScale(properties.xScale);
|
|
4061
|
+
}
|
|
4062
|
+
|
|
4063
|
+
if (properties.yScale !== undefined) {
|
|
4064
|
+
setYScale(properties.yScale);
|
|
4065
|
+
}
|
|
4066
|
+
|
|
4067
|
+
if (properties.deselectOnDblClick !== undefined) {
|
|
4068
|
+
setDeselectOnDblClick(properties.deselectOnDblClick);
|
|
4069
|
+
}
|
|
4070
|
+
|
|
4071
|
+
if (properties.deselectOnEscape !== undefined) {
|
|
4072
|
+
setDeselectOnEscape(properties.deselectOnEscape);
|
|
4073
|
+
}
|
|
4074
|
+
|
|
4075
|
+
if (properties.opacityByDensityFill !== undefined) {
|
|
4076
|
+
setOpacityByDensityFill(properties.opacityByDensityFill);
|
|
4077
|
+
}
|
|
4078
|
+
|
|
4079
|
+
if (properties.opacityInactiveMax !== undefined) {
|
|
4080
|
+
setOpacityInactiveMax(properties.opacityInactiveMax);
|
|
4081
|
+
}
|
|
4082
|
+
|
|
4083
|
+
if (properties.opacityInactiveScale !== undefined) {
|
|
4084
|
+
setOpacityInactiveScale(properties.opacityInactiveScale);
|
|
4085
|
+
}
|
|
4086
|
+
|
|
4087
|
+
if (properties.gamma !== undefined) {
|
|
4088
|
+
setGamma(properties.gamma);
|
|
4089
|
+
}
|
|
4090
|
+
|
|
4091
|
+
if (properties.annotationLineColor !== undefined) {
|
|
4092
|
+
setAnnotationLineColor(properties.annotationLineColor);
|
|
4093
|
+
}
|
|
4094
|
+
|
|
4095
|
+
if (properties.annotationLineWidth !== undefined) {
|
|
4096
|
+
setAnnotationLineWidth(properties.annotationLineWidth);
|
|
4097
|
+
}
|
|
4098
|
+
|
|
4099
|
+
if (properties.annotationHVLineLimit !== undefined) {
|
|
4100
|
+
setAnnotationHVLineLimit(properties.annotationHVLineLimit);
|
|
4101
|
+
}
|
|
4102
|
+
|
|
4103
|
+
if (properties.antiAliasing !== undefined) {
|
|
4104
|
+
setAntiAliasing(properties.antiAliasing);
|
|
4105
|
+
}
|
|
4106
|
+
|
|
4107
|
+
if (properties.pixelAligned !== undefined) {
|
|
4108
|
+
setPixelAligned(properties.pixelAligned);
|
|
4109
|
+
}
|
|
4110
|
+
|
|
4111
|
+
// setWidth and setHeight can be async when width or height are set to
|
|
4112
|
+
// 'auto'. And since draw() would have anyway been async we can just make
|
|
4113
|
+
// all calls async.
|
|
4114
|
+
return new Promise((resolve) => {
|
|
4115
|
+
window.requestAnimationFrame(() => {
|
|
4116
|
+
if (isDestroyed || !canvas) {
|
|
4117
|
+
// Instance was destroyed in between
|
|
4118
|
+
return;
|
|
4119
|
+
}
|
|
4120
|
+
updateViewAspectRatio();
|
|
4121
|
+
camera.refresh();
|
|
4122
|
+
renderer.refresh();
|
|
4123
|
+
redraw();
|
|
4124
|
+
resolve();
|
|
4125
|
+
});
|
|
4126
|
+
});
|
|
4127
|
+
};
|
|
4128
|
+
|
|
4129
|
+
/**
|
|
4130
|
+
* @param {number[]} cameraView
|
|
4131
|
+
* @param {import('./types').ScatterplotMethodOptions['preventEvent']} options
|
|
4132
|
+
*/
|
|
4133
|
+
const view = (cameraView, { preventEvent = false } = {}) => {
|
|
4134
|
+
setCameraView(cameraView);
|
|
4135
|
+
draw = true;
|
|
4136
|
+
preventEventView = preventEvent;
|
|
4137
|
+
};
|
|
4138
|
+
|
|
4139
|
+
const initCamera = () => {
|
|
4140
|
+
if (!camera) {
|
|
4141
|
+
camera = createDom2dCamera(canvas, {
|
|
4142
|
+
isFixed: cameraIsFixed,
|
|
4143
|
+
isPanInverted: [false, true],
|
|
4144
|
+
defaultMouseDownMoveAction:
|
|
4145
|
+
mouseMode === MOUSE_MODE_ROTATE ? 'rotate' : 'pan',
|
|
4146
|
+
});
|
|
4147
|
+
}
|
|
4148
|
+
|
|
4149
|
+
if (initialProperties.cameraView) {
|
|
4150
|
+
camera.setView(mat4.clone(initialProperties.cameraView));
|
|
4151
|
+
} else if (
|
|
4152
|
+
initialProperties.cameraTarget ||
|
|
4153
|
+
initialProperties.cameraDistance ||
|
|
4154
|
+
initialProperties.cameraRotation
|
|
4155
|
+
) {
|
|
4156
|
+
camera.lookAt(
|
|
4157
|
+
[...(initialProperties.cameraTarget || DEFAULT_TARGET)],
|
|
4158
|
+
initialProperties.cameraDistance || DEFAULT_DISTANCE,
|
|
4159
|
+
initialProperties.cameraRotation || DEFAULT_ROTATION,
|
|
4160
|
+
);
|
|
4161
|
+
} else {
|
|
4162
|
+
camera.setView(mat4.clone(DEFAULT_VIEW));
|
|
4163
|
+
}
|
|
4164
|
+
|
|
4165
|
+
topRightNdc = getScatterGlPos(1, 1);
|
|
4166
|
+
bottomLeftNdc = getScatterGlPos(-1, -1);
|
|
4167
|
+
};
|
|
4168
|
+
|
|
4169
|
+
/**
|
|
4170
|
+
* @param {import('./types').ScatterplotMethodOptions['preventEvent']} options
|
|
4171
|
+
*/
|
|
4172
|
+
const reset = ({ preventEvent = false } = {}) => {
|
|
4173
|
+
initCamera();
|
|
4174
|
+
updateScales();
|
|
4175
|
+
|
|
4176
|
+
if (preventEvent) {
|
|
4177
|
+
return;
|
|
4178
|
+
}
|
|
4179
|
+
|
|
4180
|
+
pubSub.publish('view', {
|
|
4181
|
+
view: camera.view,
|
|
4182
|
+
camera,
|
|
4183
|
+
xScale,
|
|
4184
|
+
yScale,
|
|
4185
|
+
});
|
|
4186
|
+
};
|
|
4187
|
+
|
|
4188
|
+
const keyUpHandler = ({ key }) => {
|
|
4189
|
+
switch (key) {
|
|
4190
|
+
case 'Escape': {
|
|
4191
|
+
if (deselectOnEscape) {
|
|
4192
|
+
deselect();
|
|
4193
|
+
}
|
|
4194
|
+
break;
|
|
4195
|
+
}
|
|
4196
|
+
default:
|
|
4197
|
+
// Nothing
|
|
4198
|
+
}
|
|
4199
|
+
};
|
|
4200
|
+
|
|
4201
|
+
const mouseEnterCanvasHandler = () => {
|
|
4202
|
+
isMouseInCanvas = true;
|
|
4203
|
+
isMouseOverCanvasChecked = true;
|
|
4204
|
+
};
|
|
4205
|
+
|
|
4206
|
+
const mouseLeaveCanvasHandler = () => {
|
|
4207
|
+
hover();
|
|
4208
|
+
isMouseInCanvas = false;
|
|
4209
|
+
isMouseOverCanvasChecked = true;
|
|
4210
|
+
draw = true;
|
|
4211
|
+
};
|
|
4212
|
+
|
|
4213
|
+
const wheelHandler = () => {
|
|
4214
|
+
draw = true;
|
|
4215
|
+
};
|
|
4216
|
+
|
|
4217
|
+
/** @type {() => void} */
|
|
4218
|
+
const clearPoints = () => {
|
|
4219
|
+
setPoints([]);
|
|
4220
|
+
pointConnections.clear();
|
|
4221
|
+
};
|
|
4222
|
+
|
|
4223
|
+
/** @type {() => void} */
|
|
4224
|
+
const clearPointConnections = () => {
|
|
4225
|
+
pointConnections.clear();
|
|
4226
|
+
};
|
|
4227
|
+
|
|
4228
|
+
/** @type {() => void} */
|
|
4229
|
+
const clearAnnotations = () => {
|
|
4230
|
+
drawAnnotations([]);
|
|
4231
|
+
};
|
|
4232
|
+
|
|
4233
|
+
/** @type {() => void} */
|
|
4234
|
+
const clear = () => {
|
|
4235
|
+
clearPoints();
|
|
4236
|
+
clearAnnotations();
|
|
4237
|
+
};
|
|
4238
|
+
|
|
4239
|
+
const resizeHandler = () => {
|
|
4240
|
+
const autoWidth = width === AUTO;
|
|
4241
|
+
const autoHeight = height === AUTO;
|
|
4242
|
+
if (autoWidth || autoHeight) {
|
|
4243
|
+
const { width: newWidth, height: newHeight } =
|
|
4244
|
+
canvas.getBoundingClientRect();
|
|
4245
|
+
|
|
4246
|
+
if (autoWidth) {
|
|
4247
|
+
setCurrentWidth(newWidth, true);
|
|
4248
|
+
}
|
|
4249
|
+
|
|
4250
|
+
if (autoHeight) {
|
|
4251
|
+
setCurrentHeight(newHeight, true);
|
|
4252
|
+
}
|
|
4253
|
+
|
|
4254
|
+
updateViewAspectRatio();
|
|
4255
|
+
updateScales();
|
|
4256
|
+
draw = true;
|
|
4257
|
+
}
|
|
4258
|
+
camera.refresh();
|
|
4259
|
+
};
|
|
4260
|
+
|
|
4261
|
+
/**
|
|
4262
|
+
* Export view as `ImageData` using custom render settings
|
|
4263
|
+
* @param {import('./types').ScatterplotMethodOptions['export']} options
|
|
4264
|
+
* @returns {Promise<ImageData>}
|
|
4265
|
+
*/
|
|
4266
|
+
const exportFnAdvanced = async (options) => {
|
|
4267
|
+
canvas.style.userSelect = 'none';
|
|
4268
|
+
|
|
4269
|
+
const dpr = window.devicePixelRatio;
|
|
4270
|
+
|
|
4271
|
+
const currPointSize = pointSize;
|
|
4272
|
+
const currWidth = width;
|
|
4273
|
+
const currHeight = height;
|
|
4274
|
+
const currRendererWidth = renderer.canvas.width / dpr;
|
|
4275
|
+
const currRendererHeight = renderer.canvas.height / dpr;
|
|
4276
|
+
const currPixelAligned = pixelAligned;
|
|
4277
|
+
const currAntiAliasing = antiAliasing;
|
|
4278
|
+
|
|
4279
|
+
const scale = options?.scale || 1;
|
|
4280
|
+
const newPointSize = Array.isArray(pointSize)
|
|
4281
|
+
? pointSize.map((s) => s * scale)
|
|
4282
|
+
: pointSize * scale;
|
|
4283
|
+
const newWidth = currentWidth * scale;
|
|
4284
|
+
const newHeight = currentHeight * scale;
|
|
4285
|
+
|
|
4286
|
+
setPointSize(newPointSize);
|
|
4287
|
+
setWidth(newWidth);
|
|
4288
|
+
setHeight(newHeight);
|
|
4289
|
+
setPixelAligned(options?.pixelAligned || pixelAligned);
|
|
4290
|
+
setAntiAliasing(options?.antiAliasing || antiAliasing);
|
|
4291
|
+
|
|
4292
|
+
renderer.resize(width, height);
|
|
4293
|
+
renderer.refresh();
|
|
4294
|
+
|
|
4295
|
+
await new Promise((resolve) => {
|
|
4296
|
+
pubSub.subscribe('draw', resolve, 1);
|
|
4297
|
+
redraw();
|
|
4298
|
+
});
|
|
4299
|
+
|
|
4300
|
+
const imageData = canvas
|
|
4301
|
+
.getContext('2d')
|
|
4302
|
+
.getImageData(0, 0, canvas.width, canvas.height);
|
|
4303
|
+
|
|
4304
|
+
renderer.resize(currRendererWidth, currRendererHeight);
|
|
4305
|
+
renderer.refresh();
|
|
4306
|
+
|
|
4307
|
+
setPointSize(currPointSize);
|
|
4308
|
+
setWidth(currWidth);
|
|
4309
|
+
setHeight(currHeight);
|
|
4310
|
+
setPixelAligned(currPixelAligned);
|
|
4311
|
+
setAntiAliasing(currAntiAliasing);
|
|
4312
|
+
|
|
4313
|
+
await new Promise((resolve) => {
|
|
4314
|
+
pubSub.subscribe('draw', resolve, 1);
|
|
4315
|
+
redraw();
|
|
4316
|
+
});
|
|
4317
|
+
|
|
4318
|
+
canvas.style.userSelect = null;
|
|
4319
|
+
|
|
4320
|
+
return imageData;
|
|
4321
|
+
};
|
|
4322
|
+
|
|
4323
|
+
/**
|
|
4324
|
+
* Export view as `ImageData` using the current render settings
|
|
4325
|
+
* @overload
|
|
4326
|
+
* @param {undefined} options
|
|
4327
|
+
* @return {ImageData}
|
|
4328
|
+
*/
|
|
4329
|
+
/**
|
|
4330
|
+
* Export view as `ImageData` using custom render settings
|
|
4331
|
+
* @overload
|
|
4332
|
+
* @param {import('./types').ScatterplotMethodOptions['export']} options
|
|
4333
|
+
* @return {Promise<ImageData>}
|
|
4334
|
+
*/
|
|
4335
|
+
/**
|
|
4336
|
+
* Export view
|
|
4337
|
+
* @param {import('./types').ScatterplotMethodOptions['export']} [options]
|
|
4338
|
+
* @returns {Promise<ImageData>}
|
|
4339
|
+
*/
|
|
4340
|
+
const exportFn = (options) => {
|
|
4341
|
+
if (options === undefined) {
|
|
4342
|
+
return canvas
|
|
4343
|
+
.getContext('2d')
|
|
4344
|
+
.getImageData(0, 0, canvas.width, canvas.height);
|
|
4345
|
+
}
|
|
4346
|
+
|
|
4347
|
+
return exportFnAdvanced(options);
|
|
4348
|
+
};
|
|
4349
|
+
|
|
4350
|
+
const init = () => {
|
|
4351
|
+
updateViewAspectRatio();
|
|
4352
|
+
initCamera();
|
|
4353
|
+
updateScales();
|
|
4354
|
+
|
|
4355
|
+
lasso = createLine(renderer.regl, {
|
|
4356
|
+
color: lassoColor,
|
|
4357
|
+
width: lassoLineWidth,
|
|
4358
|
+
is2d: true,
|
|
4359
|
+
});
|
|
4360
|
+
pointConnections = createLine(renderer.regl, {
|
|
4361
|
+
color: getColors(
|
|
4362
|
+
pointConnectionColor,
|
|
4363
|
+
pointConnectionColorActive,
|
|
4364
|
+
pointConnectionColorHover,
|
|
4365
|
+
),
|
|
4366
|
+
opacity:
|
|
4367
|
+
pointConnectionOpacity === null ? null : pointConnectionOpacity[0],
|
|
4368
|
+
width: pointConnectionSize[0],
|
|
4369
|
+
widthActive: pointConnectionSizeActive,
|
|
4370
|
+
is2d: true,
|
|
4371
|
+
});
|
|
4372
|
+
reticleHLine = createLine(renderer.regl, {
|
|
4373
|
+
color: reticleColor,
|
|
4374
|
+
width: 1,
|
|
4375
|
+
is2d: true,
|
|
4376
|
+
});
|
|
4377
|
+
reticleVLine = createLine(renderer.regl, {
|
|
4378
|
+
color: reticleColor,
|
|
4379
|
+
width: 1,
|
|
4380
|
+
is2d: true,
|
|
4381
|
+
});
|
|
4382
|
+
annotations = createLine(renderer.regl, {
|
|
4383
|
+
color: annotationLineColor,
|
|
4384
|
+
width: annotationLineWidth,
|
|
4385
|
+
is2d: true,
|
|
4386
|
+
});
|
|
4387
|
+
computePointSizeMouseDetection();
|
|
4388
|
+
|
|
4389
|
+
// Event listeners
|
|
4390
|
+
canvas.addEventListener('wheel', wheelHandler);
|
|
4391
|
+
|
|
4392
|
+
// Buffers
|
|
4393
|
+
normalPointsIndexBuffer = renderer.regl.buffer();
|
|
4394
|
+
selectedPointsIndexBuffer = renderer.regl.buffer();
|
|
4395
|
+
hoveredPointIndexBuffer = renderer.regl.buffer({
|
|
4396
|
+
usage: 'dynamic',
|
|
4397
|
+
type: 'float',
|
|
4398
|
+
length: FLOAT_BYTES * 2, // This buffer is fixed to exactly 1 point consisting of 2 coordinates
|
|
4399
|
+
});
|
|
4400
|
+
|
|
4401
|
+
colorTex = createColorTexture();
|
|
4402
|
+
encodingTex = createEncodingTexture();
|
|
4403
|
+
|
|
4404
|
+
// Set dimensions
|
|
4405
|
+
const whenSet = set({
|
|
4406
|
+
backgroundImage,
|
|
4407
|
+
width,
|
|
4408
|
+
height,
|
|
4409
|
+
actionKeyMap,
|
|
4410
|
+
});
|
|
4411
|
+
updateLassoInitiatorStyle();
|
|
4412
|
+
updateLassoLongPressIndicatorStyle();
|
|
4413
|
+
|
|
4414
|
+
// Setup event handler
|
|
4415
|
+
window.addEventListener('keyup', keyUpHandler, false);
|
|
4416
|
+
window.addEventListener('blur', blurHandler, false);
|
|
4417
|
+
window.addEventListener('mouseup', mouseUpHandler, false);
|
|
4418
|
+
window.addEventListener('mousemove', mouseMoveHandler, false);
|
|
4419
|
+
canvas.addEventListener('mousedown', mouseDownHandler, false);
|
|
4420
|
+
canvas.addEventListener('mouseenter', mouseEnterCanvasHandler, false);
|
|
4421
|
+
canvas.addEventListener('mouseleave', mouseLeaveCanvasHandler, false);
|
|
4422
|
+
canvas.addEventListener('click', mouseClickHandler, false);
|
|
4423
|
+
canvas.addEventListener('dblclick', mouseDblClickHandler, false);
|
|
4424
|
+
|
|
4425
|
+
if ('ResizeObserver' in window) {
|
|
4426
|
+
canvasObserver = new ResizeObserver(resizeHandler);
|
|
4427
|
+
canvasObserver.observe(canvas);
|
|
4428
|
+
} else {
|
|
4429
|
+
window.addEventListener('resize', resizeHandler);
|
|
4430
|
+
window.addEventListener('orientationchange', resizeHandler);
|
|
4431
|
+
}
|
|
4432
|
+
|
|
4433
|
+
whenSet.then(() => {
|
|
4434
|
+
pubSub.publish('init');
|
|
4435
|
+
});
|
|
4436
|
+
};
|
|
4437
|
+
|
|
4438
|
+
const cancelFrameListener = renderer.onFrame(() => {
|
|
4439
|
+
// Update camera: this needs to happen on every frame
|
|
4440
|
+
isViewChanged = camera.tick();
|
|
4441
|
+
|
|
4442
|
+
if (!((isPointsDrawn || isAnnotationsDrawn) && (draw || isTransitioning))) {
|
|
4443
|
+
return;
|
|
4444
|
+
}
|
|
4445
|
+
|
|
4446
|
+
if (isTransitioning && !tween(transitionDuration, transitionEasing)) {
|
|
4447
|
+
endTransition();
|
|
4448
|
+
}
|
|
4449
|
+
|
|
4450
|
+
if (isViewChanged) {
|
|
4451
|
+
topRightNdc = getScatterGlPos(1, 1);
|
|
4452
|
+
bottomLeftNdc = getScatterGlPos(-1, -1);
|
|
4453
|
+
if (opacityBy === 'density') {
|
|
4454
|
+
getNumPointsInViewDb();
|
|
4455
|
+
}
|
|
4456
|
+
}
|
|
4457
|
+
|
|
4458
|
+
renderer.render(() => {
|
|
4459
|
+
const widthRatio = canvas.width / renderer.canvas.width;
|
|
4460
|
+
const heightRatio = canvas.height / renderer.canvas.height;
|
|
4461
|
+
|
|
4462
|
+
updateProjectionMatrix(widthRatio, heightRatio);
|
|
4463
|
+
|
|
4464
|
+
if (backgroundImage?._reglType) {
|
|
4465
|
+
drawBackgroundImage();
|
|
4466
|
+
}
|
|
4467
|
+
|
|
4468
|
+
if (lassoPointsCurr.length > 2) {
|
|
4469
|
+
drawLassoPolygon();
|
|
4470
|
+
}
|
|
4471
|
+
|
|
4472
|
+
// The draw order of the following calls is important!
|
|
4473
|
+
if (!isTransitioning) {
|
|
4474
|
+
pointConnections.draw({
|
|
4475
|
+
projection: getProjection(),
|
|
4476
|
+
model: getModel(),
|
|
4477
|
+
view: getView(),
|
|
4478
|
+
});
|
|
4479
|
+
}
|
|
4480
|
+
|
|
4481
|
+
const numPoints = getNormalNumPoints();
|
|
4482
|
+
if (isPointsDrawn && numPoints > 0) {
|
|
4483
|
+
drawPointBodies();
|
|
4484
|
+
}
|
|
4485
|
+
|
|
4486
|
+
if (!mouseDown && (showReticle || drawReticleOnce)) {
|
|
4487
|
+
drawReticle();
|
|
4488
|
+
}
|
|
4489
|
+
|
|
4490
|
+
if (hoveredPoint >= 0) {
|
|
4491
|
+
drawHoveredPoint();
|
|
4492
|
+
}
|
|
4493
|
+
|
|
4494
|
+
if (selectedPoints.length > 0) {
|
|
4495
|
+
drawSelectedPoints();
|
|
4496
|
+
}
|
|
4497
|
+
|
|
4498
|
+
annotations.draw({
|
|
4499
|
+
projection: getProjection(),
|
|
4500
|
+
model: getModel(),
|
|
4501
|
+
view: getView(),
|
|
4502
|
+
});
|
|
4503
|
+
|
|
4504
|
+
lasso.draw({
|
|
4505
|
+
projection: getProjection(),
|
|
4506
|
+
model: getModel(),
|
|
4507
|
+
view: getView(),
|
|
4508
|
+
});
|
|
4509
|
+
}, canvas);
|
|
4510
|
+
|
|
4511
|
+
const renderView = {
|
|
4512
|
+
view: camera.view,
|
|
4513
|
+
isViewChanged,
|
|
4514
|
+
camera,
|
|
4515
|
+
xScale,
|
|
4516
|
+
yScale,
|
|
4517
|
+
};
|
|
4518
|
+
|
|
4519
|
+
// Publish camera change
|
|
4520
|
+
if (isViewChanged) {
|
|
4521
|
+
updateScales();
|
|
4522
|
+
|
|
4523
|
+
if (preventEventView) {
|
|
4524
|
+
preventEventView = false;
|
|
4525
|
+
} else {
|
|
4526
|
+
pubSub.publish('view', renderView);
|
|
4527
|
+
}
|
|
4528
|
+
}
|
|
4529
|
+
|
|
4530
|
+
draw = false;
|
|
4531
|
+
drawReticleOnce = false;
|
|
4532
|
+
|
|
4533
|
+
pubSub.publish('drawing', renderView, { async: false });
|
|
4534
|
+
pubSub.publish('draw', renderView);
|
|
4535
|
+
});
|
|
4536
|
+
|
|
4537
|
+
const redraw = () => {
|
|
4538
|
+
draw = true;
|
|
4539
|
+
};
|
|
4540
|
+
|
|
4541
|
+
const destroy = () => {
|
|
4542
|
+
isPointsDrawn = false;
|
|
4543
|
+
isAnnotationsDrawn = false;
|
|
4544
|
+
isDestroyed = true;
|
|
4545
|
+
cancelFrameListener();
|
|
4546
|
+
window.removeEventListener('keyup', keyUpHandler, false);
|
|
4547
|
+
window.removeEventListener('blur', blurHandler, false);
|
|
4548
|
+
window.removeEventListener('mouseup', mouseUpHandler, false);
|
|
4549
|
+
window.removeEventListener('mousemove', mouseMoveHandler, false);
|
|
4550
|
+
canvas.removeEventListener('mousedown', mouseDownHandler, false);
|
|
4551
|
+
canvas.removeEventListener('mouseenter', mouseEnterCanvasHandler, false);
|
|
4552
|
+
canvas.removeEventListener('mouseleave', mouseLeaveCanvasHandler, false);
|
|
4553
|
+
canvas.removeEventListener('click', mouseClickHandler, false);
|
|
4554
|
+
canvas.removeEventListener('dblclick', mouseDblClickHandler, false);
|
|
4555
|
+
canvas.removeEventListener('wheel', wheelHandler, false);
|
|
4556
|
+
if (canvasObserver) {
|
|
4557
|
+
canvasObserver.disconnect();
|
|
4558
|
+
} else {
|
|
4559
|
+
window.removeEventListener('resize', resizeHandler);
|
|
4560
|
+
window.removeEventListener('orientationchange', resizeHandler);
|
|
4561
|
+
}
|
|
4562
|
+
canvas = undefined;
|
|
4563
|
+
camera.dispose();
|
|
4564
|
+
camera = undefined;
|
|
4565
|
+
lasso.destroy();
|
|
4566
|
+
lassoManager.destroy();
|
|
4567
|
+
pointConnections.destroy();
|
|
4568
|
+
reticleHLine.destroy();
|
|
4569
|
+
reticleVLine.destroy();
|
|
4570
|
+
if (colorTex) {
|
|
4571
|
+
colorTex.destroy();
|
|
4572
|
+
}
|
|
4573
|
+
if (encodingTex) {
|
|
4574
|
+
encodingTex.destroy();
|
|
4575
|
+
}
|
|
4576
|
+
if (!(initialProperties.renderer || renderer.isDestroyed)) {
|
|
4577
|
+
// Since the user did not pass in an externally created renderer we can
|
|
4578
|
+
// assume that the renderer is only used by this scatter plot instance.
|
|
4579
|
+
// Therefore it's save to destroy it when this scatter plot instance is
|
|
4580
|
+
// destroyed.
|
|
4581
|
+
renderer.destroy();
|
|
4582
|
+
}
|
|
4583
|
+
pubSub.publish('destroy');
|
|
4584
|
+
pubSub.clear();
|
|
4585
|
+
};
|
|
4586
|
+
|
|
4587
|
+
init();
|
|
4588
|
+
|
|
4589
|
+
return {
|
|
4590
|
+
/**
|
|
4591
|
+
* Get whether the browser supports all necessary WebGL features
|
|
4592
|
+
* @return {boolean} If `true` the browser supports all necessary WebGL features
|
|
4593
|
+
*/
|
|
4594
|
+
get isSupported() {
|
|
4595
|
+
return renderer.isSupported;
|
|
4596
|
+
},
|
|
4597
|
+
clear: withDraw(clear),
|
|
4598
|
+
clearPoints: withDraw(clearPoints),
|
|
4599
|
+
clearPointConnections: withDraw(clearPointConnections),
|
|
4600
|
+
clearAnnotations: withDraw(clearAnnotations),
|
|
4601
|
+
createTextureFromUrl: (
|
|
4602
|
+
/** @type {string} */ url,
|
|
4603
|
+
/** @type {number} */ timeout = DEFAULT_IMAGE_LOAD_TIMEOUT,
|
|
4604
|
+
) => createTextureFromUrl(renderer.regl, url, timeout),
|
|
4605
|
+
deselect,
|
|
4606
|
+
destroy,
|
|
4607
|
+
draw: publicDraw,
|
|
4608
|
+
drawAnnotations,
|
|
4609
|
+
filter,
|
|
4610
|
+
get,
|
|
4611
|
+
getScreenPosition,
|
|
4612
|
+
hover,
|
|
4613
|
+
redraw,
|
|
4614
|
+
refresh: renderer.refresh,
|
|
4615
|
+
reset: withDraw(reset),
|
|
4616
|
+
select,
|
|
4617
|
+
set,
|
|
4618
|
+
export: exportFn,
|
|
4619
|
+
subscribe: pubSub.subscribe,
|
|
4620
|
+
unfilter,
|
|
4621
|
+
unsubscribe: pubSub.unsubscribe,
|
|
4622
|
+
view,
|
|
4623
|
+
zoomToLocation,
|
|
4624
|
+
zoomToArea,
|
|
4625
|
+
zoomToPoints,
|
|
4626
|
+
zoomToOrigin,
|
|
4627
|
+
};
|
|
4628
|
+
};
|
|
4629
|
+
|
|
4630
|
+
export default createScatterplot;
|
|
4631
|
+
|
|
4632
|
+
/**
|
|
4633
|
+
* Create spatial index from points.
|
|
4634
|
+
*
|
|
4635
|
+
* @description
|
|
4636
|
+
* The spatial index can be used with `scatterplot.draw(points, { spatialIndex })`
|
|
4637
|
+
* to drastically speed up the draw call.
|
|
4638
|
+
*
|
|
4639
|
+
* @param {import('./types').Points} points - The points for which to create the spatial index.
|
|
4640
|
+
* @param {boolean=} useWorker - Whether to create the spatial index in a worker thread or not. If `undefined`, the spatial index will be created in a worker if `points` contains more than one million entries.
|
|
4641
|
+
* @return {Promise<ArrayBuffer>} Spatial index
|
|
4642
|
+
*/
|
|
4643
|
+
const createSpatialIndex = (points, useWorker) =>
|
|
4644
|
+
toArrayOrientedPoints(points)
|
|
4645
|
+
.then((arrayPoints) => createKdbush(arrayPoints, { useWorker }))
|
|
4646
|
+
.then((index) => index.data);
|
|
4647
|
+
|
|
4648
|
+
export {
|
|
4649
|
+
createRegl,
|
|
4650
|
+
createRenderer,
|
|
4651
|
+
createSpatialIndex,
|
|
4652
|
+
createTextureFromUrl,
|
|
4653
|
+
checkSupport,
|
|
4654
|
+
};
|