@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/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
+ };