@hema-to/regl-scatterplot 1.14.1 → 1.16.0-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.
@@ -1,90 +1,93 @@
1
1
  import createPubSub from 'pub-sub-es';
2
2
  import createOriginalRegl from 'regl';
3
3
 
4
- // @flekschas/utils v0.32.2 Copyright 2023 Fritz Lekschas
5
4
  /* eslint no-param-reassign:0 */
6
-
7
5
  /**
8
6
  * Cubic in easing function
9
- * @param {number} t - The input time to be eased. Must be in [0, 1] where `0`
7
+ * @param t - The input time to be eased. Must be in [0, 1] where `0`
10
8
  * refers to the start and `1` to the end
11
- * @return {number} The eased time
9
+ * @return The eased time
12
10
  */
13
11
  const cubicIn = (t) => t * t * t;
14
-
15
12
  /**
16
13
  * Cubic in and out easing function
17
- * @param {number} t - The input time to be eased. Must be in [0, 1] where `0`
14
+ * @param t - The input time to be eased. Must be in [0, 1] where `0`
18
15
  * refers to the start and `1` to the end
19
- * @return {number} The eased time
16
+ * @return The eased time
20
17
  */
21
- const cubicInOut = (t) =>
22
- t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1;
23
-
18
+ const cubicInOut = (t) => t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1;
24
19
  /**
25
20
  * Cubic out easing function
26
- * @param {number} t - The input time to be eased. Must be in [0, 1] where `0`
21
+ * @param t - The input time to be eased. Must be in [0, 1] where `0`
27
22
  * refers to the start and `1` to the end
28
- * @return {number} The eased time
23
+ * @return The eased time
29
24
  */
30
25
  const cubicOut = (t) => --t * t * t + 1;
31
-
32
26
  /**
33
27
  * Linear easing function
34
- * @param {number} t - The input time to be eased. Must be in [0, 1] where `0`
28
+ * @param t - The input time to be eased. Must be in [0, 1] where `0`
35
29
  * refers to the start and `1` to the end
36
- * @return {number} Same as the input
30
+ * @return Same as the input
37
31
  */
38
32
  const linear = (t) => t;
39
-
40
33
  /**
41
34
  * Quadratic in easing function
42
- * @param {number} t - The input time to be eased. Must be in [0, 1] where `0`
35
+ * @param t - The input time to be eased. Must be in [0, 1] where `0`
43
36
  * refers to the start and `1` to the end
44
- * @return {number} The eased time
37
+ * @return The eased time
45
38
  */
46
39
  const quadIn = (t) => t * t;
47
-
48
40
  /**
49
41
  * Quadratic in and out easing function
50
- * @param {number} t - The input time to be eased. Must be in [0, 1] where `0`
42
+ * @param t - The input time to be eased. Must be in [0, 1] where `0`
51
43
  * refers to the start and `1` to the end
52
- * @return {number} The eased time
44
+ * @return The eased time
53
45
  */
54
- const quadInOut = (t) => (t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t);
55
-
46
+ const quadInOut = (t) => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
56
47
  /**
57
48
  * Quadratic out easing function
58
- * @param {number} t - The input time to be eased. Must be in [0, 1] where `0`
49
+ * @param t - The input time to be eased. Must be in [0, 1] where `0`
59
50
  * refers to the start and `1` to the end
60
- * @return {number} The eased time
51
+ * @return The eased time
61
52
  */
62
53
  const quadOut = (t) => t * (2 - t);
63
54
 
55
+ /**
56
+ * Restrict value to be within [min, max]
57
+ * @description About 18% faster than `Math.max(min, Math.min(max, value))`
58
+ * @param value - Value to be clamped
59
+ * @param min - Min value
60
+ * @param max - Max value
61
+ * @return Clamped value
62
+ */
64
63
  /**
65
64
  * Identity function
66
- * @type {<T>(x: T) => T}
67
- * @param {*} x - Any kind of value
68
- * @return {*} `x`
65
+ * @param x - Any kind of value
66
+ * @return `x`
69
67
  */
70
68
  const identity = (x) => x;
71
69
 
72
70
  /**
73
71
  * Check if two arrays contain the same elements
74
- * @type {<T>(a: T[], b: T[]) => Boolean}
75
- * @param {array} a - First array
76
- * @param {array} b - Second array
77
- * @return {boolean} If `true` the two arrays contain the same elements
72
+ * @param a - First array
73
+ * @param b - Second array
74
+ * @return If `true` the two arrays contain the same elements
78
75
  */
79
76
  const hasSameElements = (a, b) => {
80
- if (a === b) return true;
81
- if (a.length !== b.length) return false;
82
- const aSet = new Set(a);
83
- const bSet = new Set(b);
84
- // Since the arrays could contain duplicates, we have to check the set length
85
- // as well
86
- if (aSet.size !== bSet.size) return false;
87
- return b.every((element) => aSet.has(element));
77
+ if (a === b) {
78
+ return true;
79
+ }
80
+ if (a.length !== b.length) {
81
+ return false;
82
+ }
83
+ const aSet = new Set(a);
84
+ const bSet = new Set(b);
85
+ // Since the arrays could contain duplicates, we have to check the set length
86
+ // as well
87
+ if (aSet.size !== bSet.size) {
88
+ return false;
89
+ }
90
+ return b.every((element) => aSet.has(element));
88
91
  };
89
92
 
90
93
  /**
@@ -93,11 +96,10 @@ const hasSameElements = (a, b) => {
93
96
  * @description
94
97
  * This is identical but much faster than `Math.hypot(...v)`
95
98
  *
96
- * @param {number[]} v - Numerical vector
97
- * @return {number} L2 norm
99
+ * @param v - Numerical vector
100
+ * @return L2 norm
98
101
  */
99
102
  const l2Norm = (v) => Math.sqrt(v.reduce((sum, x) => sum + x ** 2, 0));
100
-
101
103
  /**
102
104
  * Get the maximum number of a vector while ignoring NaNs
103
105
  *
@@ -105,164 +107,158 @@ const l2Norm = (v) => Math.sqrt(v.reduce((sum, x) => sum + x ** 2, 0));
105
107
  * This version is muuuch faster than `Math.max(...v)` and supports vectors
106
108
  * longer than 256^2, which is a limitation of `Math.max.apply(null, v)`.
107
109
  *
108
- * @param {number[]} v - Numerical vector
109
- * @return {number} The largest number
110
+ * @param v - Numerical vector
111
+ * @return The largest number
110
112
  */
111
- const max$1 = (v) =>
112
- v.reduce((_max, a) => (a > _max ? a : _max), -Infinity);
113
-
113
+ const max$1 = (v) => v.reduce((_max, a) => (a > _max ? a : _max), Number.NEGATIVE_INFINITY);
114
114
  /**
115
115
  * Initialize an array of a certain length using a mapping function
116
116
  *
117
117
  * @description
118
118
  * This is equivalent to `Array.from({ length }, mapFn)` but about 60% faster
119
119
  *
120
- * @param {number} length - Size of the array
121
- * @param {function} mapFn - Mapping function
122
- * @return {array} Initialized array
123
- * @type {<T = number>(length: number, mapFn: (i: number, length: number) => T) => T[]}
120
+ * @param length - Size of the array
121
+ * @param mapFn - Mapping function
122
+ * @return Initialized array
124
123
  */
125
- const rangeMap = (length, mapFn = (x) => x) => {
126
- const out = [];
127
- for (let i = 0; i < length; i++) {
128
- out.push(mapFn(i, length));
129
- }
130
- return out;
124
+ const rangeMap = (length, mapFn = identity) => {
125
+ const out = [];
126
+ for (let i = 0; i < length; i++) {
127
+ out.push(mapFn(i, length));
128
+ }
129
+ return out;
131
130
  };
132
-
133
131
  /**
134
132
  * Get the unique union of two vectors of integers
135
- * @param {number[]} v - First vector of integers
136
- * @param {number[]} w - Second vector of integers
137
- * @return {number[]} Unique union of `v` and `w`
133
+ * @param v - First vector of integers
134
+ * @param w - Second vector of integers
135
+ * @return Unique union of `v` and `w`
138
136
  */
139
137
  const unionIntegers = (v, w) => {
140
- const a = [];
141
- v.forEach((x) => {
142
- a[x] = true;
143
- });
144
- w.forEach((x) => {
145
- a[x] = true;
146
- });
147
- return a.reduce((union, value, i) => {
148
- if (value) union.push(i);
149
- return union;
150
- }, []);
138
+ const a = [];
139
+ v.forEach((x) => {
140
+ a[x] = true;
141
+ });
142
+ w.forEach((x) => {
143
+ a[x] = true;
144
+ });
145
+ return a.reduce((union, value, i) => {
146
+ if (value)
147
+ union.push(i);
148
+ return union;
149
+ }, []);
151
150
  };
152
151
 
153
152
  /**
154
153
  * Assign properties, constructors, etc. to an object
155
154
  *
156
- * @param {object} target - The target object that gets `sources` assigned to it
157
- * @param {}
158
- * @return {object}
155
+ * @param target - The target object that gets `sources` assigned to it
156
+ * @param sources - The sources to be assigned to the target object
157
+ * @return An object representing the union of the target and sources objects
159
158
  */
160
159
  const assign = (target, ...sources) => {
161
- sources.forEach((source) => {
162
- // eslint-disable-next-line no-shadow
163
- const descriptors = Object.keys(source).reduce((descriptors, key) => {
164
- descriptors[key] = Object.getOwnPropertyDescriptor(source, key);
165
- return descriptors;
166
- }, {});
167
-
168
- // By default, Object.assign copies enumerable Symbols, too
169
- Object.getOwnPropertySymbols(source).forEach((symbol) => {
170
- const descriptor = Object.getOwnPropertyDescriptor(source, symbol);
171
- if (descriptor.enumerable) {
172
- descriptors[symbol] = descriptor;
173
- }
160
+ sources.forEach((source) => {
161
+ // eslint-disable-next-line no-shadow
162
+ const descriptors = Object.keys(source).reduce((descriptors, key) => {
163
+ descriptors[key] = Object.getOwnPropertyDescriptor(source, key);
164
+ return descriptors;
165
+ }, {});
166
+ // By default, Object.assign copies enumerable Symbols, too
167
+ Object.getOwnPropertySymbols(source).forEach((symbol) => {
168
+ const descriptor = Object.getOwnPropertyDescriptor(source, symbol);
169
+ if (descriptor?.enumerable) {
170
+ descriptors[symbol] = descriptor;
171
+ }
172
+ });
173
+ Object.defineProperties(target, descriptors);
174
174
  });
175
- Object.defineProperties(target, descriptors);
176
- });
177
- return target;
175
+ return target;
178
176
  };
179
177
 
180
178
  /**
181
179
  * Convenience function to compose functions
182
- * @param {...function} fns - Array of functions
183
- * @return {function} The composed function
180
+ * @param fns - Array of functions
181
+ * @return The composed function
184
182
  */
185
- const pipe =
186
- (...fns) =>
187
- /**
188
- * @param {*} x - Some value
189
- * @return {*} Output of the composed function
190
- */
191
- (x) =>
192
- fns.reduce((y, f) => f(y), x);
193
-
183
+ function pipe(...fns) {
184
+ return (x) => fns.reduce((y, f) => f(y), x);
185
+ }
194
186
  /**
195
187
  * Assign a constructor to the object
196
- * @param {function} constructor - Constructor functions
188
+ * @param constructorFn - Constructor functions
197
189
  */
198
- const withConstructor = (constructor) => (self) =>
199
- assign(
200
- {
201
- __proto__: {
202
- constructor,
203
- },
190
+ const withConstructor = (constructorFn) => (self) => assign({
191
+ __proto__: {
192
+ constructor: constructorFn,
204
193
  },
205
- self
206
- );
207
-
194
+ }, self);
208
195
  /**
209
196
  * Assign a static property to an object
210
- * @param {string} name - Name of the property
211
- * @param {*} value - Static value
197
+ * @param name - Name of the property
198
+ * @param value - Static value
212
199
  */
213
- const withStaticProperty = (name, value) => (self) =>
214
- assign(self, {
200
+ const withStaticProperty = (name, value) => (self) => assign(self, {
215
201
  get [name]() {
216
- return value;
202
+ return value;
217
203
  },
218
- });
204
+ });
219
205
 
206
+ /**
207
+ * L distance between a pair of vectors
208
+ *
209
+ * @description
210
+ * Identical but much faster than `lDist(l)([fromX, fromY], [toX, toY])`
211
+ *
212
+ * @param l - Defines the Lp space
213
+ */
220
214
  /**
221
215
  * L2 distance between a pair of points
222
216
  *
223
217
  * @description
224
218
  * Identical but much faster than `l2Dist([fromX, fromY], [toX, toY])`
225
219
  *
226
- * @param {number} fromX - X coordinate of the first point
227
- * @param {number} fromY - Y coordinate of the first point
228
- * @param {number} toX - X coordinate of the second point
229
- * @param {number} toY - Y coordinate of the first point
230
- * @return {number} L2 distance
220
+ * @paramfromX - X coordinate of the first point
221
+ * @paramfromY - Y coordinate of the first point
222
+ * @paramtoX - X coordinate of the second point
223
+ * @paramtoY - Y coordinate of the first point
224
+ * @returnL2 distance
231
225
  */
232
- const l2PointDist = (fromX, fromY, toX, toY) =>
233
- Math.sqrt((fromX - toX) ** 2 + (fromY - toY) ** 2);
226
+ const l2PointDist = (fromX, fromY, toX, toY) => Math.sqrt((fromX - toX) ** 2 + (fromY - toY) ** 2);
234
227
 
235
228
  /**
236
229
  * Create a worker from a function
237
- * @param {function} fn - Function to be turned into a worker
238
- * @return {Worker} Worker function
230
+ * @param fn - Function to be turned into a worker
231
+ * @return Worker function
239
232
  */
240
- const createWorker$1 = (fn) =>
241
- new Worker(
242
- window.URL.createObjectURL(
243
- new Blob([`(${fn.toString()})()`], { type: 'text/javascript' })
244
- )
245
- );
233
+ const createWorker$1 = (fn) => new Worker(window.URL.createObjectURL(new Blob([`(${fn.toString()})()`], { type: "text/javascript" })));
246
234
 
235
+ /**
236
+ * Debounce a function call.
237
+ *
238
+ * @description
239
+ * Function calls are delayed by `wait` milliseconds and only one out of
240
+ * multiple function calls is executed.
241
+ *
242
+ * @param fn - Function to be debounced
243
+ * @param wait - Number of milliseconds to debounce the function call.
244
+ * @return Debounced function
245
+ */
247
246
  /**
248
247
  * Get a promise that resolves after the next `n` animation frames
249
- * @param {number} n - Number of animation frames to wait
250
- * @return {Promise} A promise that resolves after the next `n` animation frames
248
+ * @param n - Number of animation frames to wait
249
+ * @return A promise that resolves after the next `n` animation frames
251
250
  */
252
- const nextAnimationFrame = (n = 1) =>
253
- new Promise((resolve) => {
251
+ const nextAnimationFrame = (n = 1) => new Promise((resolve) => {
254
252
  let i = 0;
255
-
256
- const raf = () =>
257
- requestAnimationFrame(() => {
253
+ const raf = () => requestAnimationFrame(() => {
258
254
  i++;
259
- if (i < n) raf();
260
- else resolve();
261
- });
262
-
255
+ if (i < n)
256
+ raf();
257
+ else
258
+ resolve(undefined);
259
+ });
263
260
  raf();
264
- });
265
-
261
+ });
266
262
  /**
267
263
  * Throttle and debounce a function call
268
264
  *
@@ -285,83 +281,64 @@ const nextAnimationFrame = (n = 1) =>
285
281
  * 6. No call => nothing
286
282
  * 7. No call => f(args5) called due to debouncing
287
283
  *
288
- * @param {functon} fn - Function to be throttled and debounced
289
- * @param {number} throttleTime - Throttle intevals in milliseconds
290
- * @param {number} debounceTime - Debounce wait time in milliseconds. By default
291
- * this is the same as `throttleTime`.
292
- * @return {function} - Throttled and debounced function
284
+ * @param fn - Function to be throttled and debounced
285
+ * @param throttleTime - Throttle intevals in milliseconds
286
+ * @param debounceTime - Debounce wait time in milliseconds. By default this is
287
+ * the same as `throttleTime`.
288
+ * @return Throttled and debounced function
293
289
  */
294
- const throttleAndDebounce = (fn, throttleTime, debounceTime = null) => {
295
- let timeout;
296
- let blockedCalls = 0;
297
-
298
- // eslint-disable-next-line no-param-reassign
299
- debounceTime = debounceTime === null ? throttleTime : debounceTime;
300
-
301
- const debounced = (...args) => {
302
- const later = () => {
303
- // Since we throttle and debounce we should check whether there were
304
- // actually multiple attempts to call this function after the most recent
305
- // throttled call. If there were no more calls we don't have to call
306
- // the function again.
307
- if (blockedCalls > 0) {
308
- fn(...args);
309
- blockedCalls = 0;
310
- }
290
+ const throttleAndDebounce = (fn, throttleTime, debounceTime) => {
291
+ let timeout;
292
+ let blockedCalls = 0;
293
+ debounceTime = debounceTime === null ? throttleTime : debounceTime;
294
+ const debounced = (...args) => {
295
+ const later = () => {
296
+ // Since we throttle and debounce we should check whether there were
297
+ // actually multiple attempts to call this function after the most recent
298
+ // throttled call. If there were no more calls we don't have to call
299
+ // the function again.
300
+ if (blockedCalls > 0) {
301
+ fn(...args);
302
+ blockedCalls = 0;
303
+ }
304
+ };
305
+ clearTimeout(timeout);
306
+ timeout = setTimeout(later, debounceTime);
311
307
  };
312
-
313
- clearTimeout(timeout);
314
- timeout = setTimeout(later, debounceTime);
315
- };
316
-
317
- let isWaiting = false;
318
- const throttledAndDebounced = (...args) => {
319
- if (!isWaiting) {
320
- fn(...args);
321
- debounced(...args);
322
-
323
- isWaiting = true;
324
- blockedCalls = 0;
325
-
326
- setTimeout(() => {
308
+ let isWaiting = false;
309
+ const throttledAndDebounced = (...args) => {
310
+ if (isWaiting) {
311
+ blockedCalls++;
312
+ debounced(...args);
313
+ }
314
+ else {
315
+ fn(...args);
316
+ debounced(...args);
317
+ isWaiting = true;
318
+ blockedCalls = 0;
319
+ setTimeout(() => {
320
+ isWaiting = false;
321
+ }, throttleTime);
322
+ }
323
+ };
324
+ throttledAndDebounced.reset = () => {
327
325
  isWaiting = false;
328
- }, throttleTime);
329
- } else {
330
- blockedCalls++;
331
- debounced(...args);
332
- }
333
- };
334
-
335
- throttledAndDebounced.reset = () => {
336
- isWaiting = false;
337
- };
338
-
339
- throttledAndDebounced.cancel = () => {
340
- clearTimeout(timeout);
341
- };
342
-
343
- throttledAndDebounced.now = (...args) => fn(...args);
344
-
345
- return throttledAndDebounced;
326
+ };
327
+ throttledAndDebounced.cancel = () => {
328
+ clearTimeout(timeout);
329
+ };
330
+ throttledAndDebounced.now = (...args) => fn(...args);
331
+ return throttledAndDebounced;
346
332
  };
347
333
 
348
334
  /**
349
335
  * Common utilities
350
336
  * @module glMatrix
351
337
  */
338
+
352
339
  // Configuration Constants
353
340
  var EPSILON = 0.000001;
354
- var ARRAY_TYPE = typeof Float32Array !== 'undefined' ? Float32Array : Array;
355
- if (!Math.hypot) Math.hypot = function () {
356
- var y = 0,
357
- i = arguments.length;
358
-
359
- while (i--) {
360
- y += arguments[i] * arguments[i];
361
- }
362
-
363
- return Math.sqrt(y);
364
- };
341
+ var ARRAY_TYPE = typeof Float32Array !== "undefined" ? Float32Array : Array;
365
342
 
366
343
  /**
367
344
  * 4x4 Matrix<br>Format: column-major, when typed out it looks like row-major<br>The matrices are being post multiplied.
@@ -373,10 +350,8 @@ if (!Math.hypot) Math.hypot = function () {
373
350
  *
374
351
  * @returns {mat4} a new 4x4 matrix
375
352
  */
376
-
377
353
  function create$2() {
378
354
  var out = new ARRAY_TYPE(16);
379
-
380
355
  if (ARRAY_TYPE != Float32Array) {
381
356
  out[1] = 0;
382
357
  out[2] = 0;
@@ -391,20 +366,19 @@ function create$2() {
391
366
  out[13] = 0;
392
367
  out[14] = 0;
393
368
  }
394
-
395
369
  out[0] = 1;
396
370
  out[5] = 1;
397
371
  out[10] = 1;
398
372
  out[15] = 1;
399
373
  return out;
400
374
  }
375
+
401
376
  /**
402
377
  * Creates a new mat4 initialized with values from an existing matrix
403
378
  *
404
379
  * @param {ReadonlyMat4} a matrix to clone
405
380
  * @returns {mat4} a new 4x4 matrix
406
381
  */
407
-
408
382
  function clone(a) {
409
383
  var out = new ARRAY_TYPE(16);
410
384
  out[0] = a[0];
@@ -425,31 +399,31 @@ function clone(a) {
425
399
  out[15] = a[15];
426
400
  return out;
427
401
  }
402
+
428
403
  /**
429
404
  * Inverts a mat4
430
405
  *
431
406
  * @param {mat4} out the receiving matrix
432
407
  * @param {ReadonlyMat4} a the source matrix
433
- * @returns {mat4} out
408
+ * @returns {mat4 | null} out, or null if source matrix is not invertible
434
409
  */
435
-
436
410
  function invert(out, a) {
437
411
  var a00 = a[0],
438
- a01 = a[1],
439
- a02 = a[2],
440
- a03 = a[3];
412
+ a01 = a[1],
413
+ a02 = a[2],
414
+ a03 = a[3];
441
415
  var a10 = a[4],
442
- a11 = a[5],
443
- a12 = a[6],
444
- a13 = a[7];
416
+ a11 = a[5],
417
+ a12 = a[6],
418
+ a13 = a[7];
445
419
  var a20 = a[8],
446
- a21 = a[9],
447
- a22 = a[10],
448
- a23 = a[11];
420
+ a21 = a[9],
421
+ a22 = a[10],
422
+ a23 = a[11];
449
423
  var a30 = a[12],
450
- a31 = a[13],
451
- a32 = a[14],
452
- a33 = a[15];
424
+ a31 = a[13],
425
+ a32 = a[14],
426
+ a33 = a[15];
453
427
  var b00 = a00 * a11 - a01 * a10;
454
428
  var b01 = a00 * a12 - a02 * a10;
455
429
  var b02 = a00 * a13 - a03 * a10;
@@ -461,14 +435,13 @@ function invert(out, a) {
461
435
  var b08 = a20 * a33 - a23 * a30;
462
436
  var b09 = a21 * a32 - a22 * a31;
463
437
  var b10 = a21 * a33 - a23 * a31;
464
- var b11 = a22 * a33 - a23 * a32; // Calculate the determinant
438
+ var b11 = a22 * a33 - a23 * a32;
465
439
 
440
+ // Calculate the determinant
466
441
  var det = b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06;
467
-
468
442
  if (!det) {
469
443
  return null;
470
444
  }
471
-
472
445
  det = 1.0 / det;
473
446
  out[0] = (a11 * b11 - a12 * b10 + a13 * b09) * det;
474
447
  out[1] = (a02 * b10 - a01 * b11 - a03 * b09) * det;
@@ -488,6 +461,7 @@ function invert(out, a) {
488
461
  out[15] = (a20 * b03 - a21 * b01 + a22 * b00) * det;
489
462
  return out;
490
463
  }
464
+
491
465
  /**
492
466
  * Multiplies two mat4s
493
467
  *
@@ -496,29 +470,29 @@ function invert(out, a) {
496
470
  * @param {ReadonlyMat4} b the second operand
497
471
  * @returns {mat4} out
498
472
  */
499
-
500
473
  function multiply(out, a, b) {
501
474
  var a00 = a[0],
502
- a01 = a[1],
503
- a02 = a[2],
504
- a03 = a[3];
475
+ a01 = a[1],
476
+ a02 = a[2],
477
+ a03 = a[3];
505
478
  var a10 = a[4],
506
- a11 = a[5],
507
- a12 = a[6],
508
- a13 = a[7];
479
+ a11 = a[5],
480
+ a12 = a[6],
481
+ a13 = a[7];
509
482
  var a20 = a[8],
510
- a21 = a[9],
511
- a22 = a[10],
512
- a23 = a[11];
483
+ a21 = a[9],
484
+ a22 = a[10],
485
+ a23 = a[11];
513
486
  var a30 = a[12],
514
- a31 = a[13],
515
- a32 = a[14],
516
- a33 = a[15]; // Cache only the current line of the second matrix
487
+ a31 = a[13],
488
+ a32 = a[14],
489
+ a33 = a[15];
517
490
 
491
+ // Cache only the current line of the second matrix
518
492
  var b0 = b[0],
519
- b1 = b[1],
520
- b2 = b[2],
521
- b3 = b[3];
493
+ b1 = b[1],
494
+ b2 = b[2],
495
+ b3 = b[3];
522
496
  out[0] = b0 * a00 + b1 * a10 + b2 * a20 + b3 * a30;
523
497
  out[1] = b0 * a01 + b1 * a11 + b2 * a21 + b3 * a31;
524
498
  out[2] = b0 * a02 + b1 * a12 + b2 * a22 + b3 * a32;
@@ -549,6 +523,7 @@ function multiply(out, a, b) {
549
523
  out[15] = b0 * a03 + b1 * a13 + b2 * a23 + b3 * a33;
550
524
  return out;
551
525
  }
526
+
552
527
  /**
553
528
  * Creates a matrix from a vector translation
554
529
  * This is equivalent to (but much faster than):
@@ -560,7 +535,6 @@ function multiply(out, a, b) {
560
535
  * @param {ReadonlyVec3} v Translation vector
561
536
  * @returns {mat4} out
562
537
  */
563
-
564
538
  function fromTranslation(out, v) {
565
539
  out[0] = 1;
566
540
  out[1] = 0;
@@ -580,6 +554,7 @@ function fromTranslation(out, v) {
580
554
  out[15] = 1;
581
555
  return out;
582
556
  }
557
+
583
558
  /**
584
559
  * Creates a matrix from a vector scaling
585
560
  * This is equivalent to (but much faster than):
@@ -591,7 +566,6 @@ function fromTranslation(out, v) {
591
566
  * @param {ReadonlyVec3} v Scaling vector
592
567
  * @returns {mat4} out
593
568
  */
594
-
595
569
  function fromScaling(out, v) {
596
570
  out[0] = v[0];
597
571
  out[1] = 0;
@@ -611,6 +585,7 @@ function fromScaling(out, v) {
611
585
  out[15] = 1;
612
586
  return out;
613
587
  }
588
+
614
589
  /**
615
590
  * Creates a matrix from a given angle around a given axis
616
591
  * This is equivalent to (but much faster than):
@@ -623,26 +598,24 @@ function fromScaling(out, v) {
623
598
  * @param {ReadonlyVec3} axis the axis to rotate around
624
599
  * @returns {mat4} out
625
600
  */
626
-
627
601
  function fromRotation(out, rad, axis) {
628
602
  var x = axis[0],
629
- y = axis[1],
630
- z = axis[2];
631
- var len = Math.hypot(x, y, z);
603
+ y = axis[1],
604
+ z = axis[2];
605
+ var len = Math.sqrt(x * x + y * y + z * z);
632
606
  var s, c, t;
633
-
634
607
  if (len < EPSILON) {
635
608
  return null;
636
609
  }
637
-
638
610
  len = 1 / len;
639
611
  x *= len;
640
612
  y *= len;
641
613
  z *= len;
642
614
  s = Math.sin(rad);
643
615
  c = Math.cos(rad);
644
- t = 1 - c; // Perform rotation-specific matrix multiplication
616
+ t = 1 - c;
645
617
 
618
+ // Perform rotation-specific matrix multiplication
646
619
  out[0] = x * x * t + c;
647
620
  out[1] = y * x * t + z * s;
648
621
  out[2] = z * x * t - y * s;
@@ -661,6 +634,7 @@ function fromRotation(out, rad, axis) {
661
634
  out[15] = 1;
662
635
  return out;
663
636
  }
637
+
664
638
  /**
665
639
  * Returns the translation vector component of a transformation
666
640
  * matrix. If a matrix is built with fromRotationTranslation,
@@ -670,24 +644,23 @@ function fromRotation(out, rad, axis) {
670
644
  * @param {ReadonlyMat4} mat Matrix to be decomposed (input)
671
645
  * @return {vec3} out
672
646
  */
673
-
674
647
  function getTranslation(out, mat) {
675
648
  out[0] = mat[12];
676
649
  out[1] = mat[13];
677
650
  out[2] = mat[14];
678
651
  return out;
679
652
  }
653
+
680
654
  /**
681
655
  * Returns the scaling factor component of a transformation
682
656
  * matrix. If a matrix is built with fromRotationTranslationScale
683
- * with a normalized Quaternion paramter, the returned vector will be
657
+ * with a normalized Quaternion parameter, the returned vector will be
684
658
  * the same as the scaling vector
685
659
  * originally supplied.
686
660
  * @param {vec3} out Vector to receive scaling factor component
687
661
  * @param {ReadonlyMat4} mat Matrix to be decomposed (input)
688
662
  * @return {vec3} out
689
663
  */
690
-
691
664
  function getScaling(out, mat) {
692
665
  var m11 = mat[0];
693
666
  var m12 = mat[1];
@@ -698,9 +671,9 @@ function getScaling(out, mat) {
698
671
  var m31 = mat[8];
699
672
  var m32 = mat[9];
700
673
  var m33 = mat[10];
701
- out[0] = Math.hypot(m11, m12, m13);
702
- out[1] = Math.hypot(m21, m22, m23);
703
- out[2] = Math.hypot(m31, m32, m33);
674
+ out[0] = Math.sqrt(m11 * m11 + m12 * m12 + m13 * m13);
675
+ out[1] = Math.sqrt(m21 * m21 + m22 * m22 + m23 * m23);
676
+ out[2] = Math.sqrt(m31 * m31 + m32 * m32 + m33 * m33);
704
677
  return out;
705
678
  }
706
679
 
@@ -714,19 +687,17 @@ function getScaling(out, mat) {
714
687
  *
715
688
  * @returns {vec4} a new 4D vector
716
689
  */
717
-
718
690
  function create$1() {
719
691
  var out = new ARRAY_TYPE(4);
720
-
721
692
  if (ARRAY_TYPE != Float32Array) {
722
693
  out[0] = 0;
723
694
  out[1] = 0;
724
695
  out[2] = 0;
725
696
  out[3] = 0;
726
697
  }
727
-
728
698
  return out;
729
699
  }
700
+
730
701
  /**
731
702
  * Transforms the vec4 with a mat4.
732
703
  *
@@ -735,18 +706,18 @@ function create$1() {
735
706
  * @param {ReadonlyMat4} m matrix to transform with
736
707
  * @returns {vec4} out
737
708
  */
738
-
739
709
  function transformMat4(out, a, m) {
740
710
  var x = a[0],
741
- y = a[1],
742
- z = a[2],
743
- w = a[3];
711
+ y = a[1],
712
+ z = a[2],
713
+ w = a[3];
744
714
  out[0] = m[0] * x + m[4] * y + m[8] * z + m[12] * w;
745
715
  out[1] = m[1] * x + m[5] * y + m[9] * z + m[13] * w;
746
716
  out[2] = m[2] * x + m[6] * y + m[10] * z + m[14] * w;
747
717
  out[3] = m[3] * x + m[7] * y + m[11] * z + m[15] * w;
748
718
  return out;
749
719
  }
720
+
750
721
  /**
751
722
  * Perform some operation over an array of vec4s.
752
723
  *
@@ -759,26 +730,21 @@ function transformMat4(out, a, m) {
759
730
  * @returns {Array} a
760
731
  * @function
761
732
  */
762
-
763
733
  (function () {
764
734
  var vec = create$1();
765
735
  return function (a, stride, offset, count, fn, arg) {
766
736
  var i, l;
767
-
768
737
  if (!stride) {
769
738
  stride = 4;
770
739
  }
771
-
772
740
  if (!offset) {
773
741
  offset = 0;
774
742
  }
775
-
776
743
  if (count) {
777
744
  l = Math.min(count * stride + offset, a.length);
778
745
  } else {
779
746
  l = a.length;
780
747
  }
781
-
782
748
  for (i = offset; i < l; i += stride) {
783
749
  vec[0] = a[i];
784
750
  vec[1] = a[i + 1];
@@ -790,7 +756,6 @@ function transformMat4(out, a, m) {
790
756
  a[i + 2] = vec[2];
791
757
  a[i + 3] = vec[3];
792
758
  }
793
-
794
759
  return a;
795
760
  };
796
761
  })();
@@ -805,36 +770,29 @@ function transformMat4(out, a, m) {
805
770
  *
806
771
  * @returns {vec2} a new 2D vector
807
772
  */
808
-
809
773
  function create() {
810
774
  var out = new ARRAY_TYPE(2);
811
-
812
775
  if (ARRAY_TYPE != Float32Array) {
813
776
  out[0] = 0;
814
777
  out[1] = 0;
815
778
  }
816
-
817
779
  return out;
818
780
  }
781
+
819
782
  /**
820
- * Get the angle between two 2D vectors
783
+ * Get the smallest angle between two 2D vectors
821
784
  * @param {ReadonlyVec2} a The first operand
822
785
  * @param {ReadonlyVec2} b The second operand
823
786
  * @returns {Number} The angle in radians
824
787
  */
825
-
826
788
  function angle(a, b) {
827
- var x1 = a[0],
828
- y1 = a[1],
829
- x2 = b[0],
830
- y2 = b[1],
831
- // mag is the product of the magnitudes of a and b
832
- mag = Math.sqrt(x1 * x1 + y1 * y1) * Math.sqrt(x2 * x2 + y2 * y2),
833
- // mag &&.. short circuits if mag == 0
834
- cosine = mag && (x1 * x2 + y1 * y2) / mag; // Math.min(Math.max(cosine, -1), 1) clamps the cosine between -1 and 1
835
-
836
- return Math.acos(Math.min(Math.max(cosine, -1), 1));
789
+ var ax = a[0],
790
+ ay = a[1],
791
+ bx = b[0],
792
+ by = b[1];
793
+ return Math.abs(Math.atan2(ay * bx - ax * by, ax * bx + ay * by));
837
794
  }
795
+
838
796
  /**
839
797
  * Perform some operation over an array of vec2s.
840
798
  *
@@ -847,26 +805,21 @@ function angle(a, b) {
847
805
  * @returns {Array} a
848
806
  * @function
849
807
  */
850
-
851
808
  (function () {
852
809
  var vec = create();
853
810
  return function (a, stride, offset, count, fn, arg) {
854
811
  var i, l;
855
-
856
812
  if (!stride) {
857
813
  stride = 2;
858
814
  }
859
-
860
815
  if (!offset) {
861
816
  offset = 0;
862
817
  }
863
-
864
818
  if (count) {
865
819
  l = Math.min(count * stride + offset, a.length);
866
820
  } else {
867
821
  l = a.length;
868
822
  }
869
-
870
823
  for (i = offset; i < l; i += stride) {
871
824
  vec[0] = a[i];
872
825
  vec[1] = a[i + 1];
@@ -874,7 +827,6 @@ function angle(a, b) {
874
827
  a[i] = vec[0];
875
828
  a[i + 1] = vec[1];
876
829
  }
877
-
878
830
  return a;
879
831
  };
880
832
  })();
@@ -1467,10 +1419,10 @@ function earcut(data, holeIndices, dim = 2) {
1467
1419
 
1468
1420
  // if the shape is not too simple, we'll use z-order curve hash later; calculate polygon bbox
1469
1421
  if (data.length > 80 * dim) {
1470
- minX = Infinity;
1471
- minY = Infinity;
1472
- let maxX = -Infinity;
1473
- let maxY = -Infinity;
1422
+ minX = data[0];
1423
+ minY = data[1];
1424
+ let maxX = minX;
1425
+ let maxY = minY;
1474
1426
 
1475
1427
  for (let i = dim; i < outerLen; i += dim) {
1476
1428
  const x = data[i];
@@ -1812,7 +1764,7 @@ function pointInTriangleExceptFirst(ax, ay, bx, by, cx, cy, px, py) {
1812
1764
 
1813
1765
  // check if a diagonal between two polygon nodes is valid (lies in polygon interior)
1814
1766
  function isValidDiagonal(a, b) {
1815
- return a.next.i !== b.i && a.prev.i !== b.i && !intersectsPolygon(a, b) && // dones't intersect other edges
1767
+ return a.next.i !== b.i && a.prev.i !== b.i && !intersectsPolygon(a, b) && // doesn't intersect other edges
1816
1768
  (locallyInside(a, b) && locallyInside(b, a) && middleInside(a, b) && // locally visible
1817
1769
  (area(a.prev, a, b.prev) || area(a, b.prev, b)) || // does not create opposite-facing sectors
1818
1770
  equals(a, b) && area(a.prev, a, a.next) > 0 && area(b.prev, b, b.next) > 0); // special zero-length case
@@ -2426,121 +2378,327 @@ const createLine = (regl, { projection = I, model = I, view = I, points = [], co
2426
2378
  };
2427
2379
  };
2428
2380
 
2429
- /**
2430
- * KDBush - A fast static index for 2D points
2431
- * @license ISC License
2432
- * @copyright Vladimir Agafonkin 2018
2433
- * @version 4.0.2
2434
- * @see https://github.com/mourner/kdbush/
2435
- */
2436
- var createKDBushClass = () => {
2437
- const ARRAY_TYPES = [
2438
- Int8Array,
2439
- Uint8Array,
2440
- Uint8ClampedArray,
2441
- Int16Array,
2442
- Uint16Array,
2443
- Int32Array,
2444
- Uint32Array,
2445
- Float32Array,
2446
- Float64Array,
2447
- ];
2381
+ var version = "1.16.0-hemato.0";
2448
2382
 
2449
- /** @typedef {Int8ArrayConstructor | Uint8ArrayConstructor | Uint8ClampedArrayConstructor | Int16ArrayConstructor | Uint16ArrayConstructor | Int32ArrayConstructor | Uint32ArrayConstructor | Float32ArrayConstructor | Float64ArrayConstructor} TypedArrayConstructor */
2383
+ const FRAGMENT_SHADER$2 = `
2384
+ precision mediump float;
2450
2385
 
2451
- const VERSION = 1; // serialized format version
2452
- const HEADER_SIZE = 8;
2386
+ uniform sampler2D texture;
2453
2387
 
2454
- class KDBush {
2455
- /**
2456
- * Creates an index from raw `ArrayBuffer` data.
2457
- * @param {ArrayBuffer} data
2458
- */
2459
- static from(data) {
2460
- if (!(data instanceof ArrayBuffer)) {
2461
- throw new Error('Data must be an instance of ArrayBuffer.');
2462
- }
2463
- const [magic, versionAndType] = new Uint8Array(data, 0, 2);
2464
- if (magic !== 0xdb) {
2465
- throw new Error('Data does not appear to be in a KDBush format.');
2466
- }
2467
- const version = versionAndType >> 4;
2468
- if (version !== VERSION) {
2469
- throw new Error(`Got v${version} data when expected v${VERSION}.`);
2470
- }
2471
- const ArrayType = ARRAY_TYPES[versionAndType & 0x0f];
2472
- if (!ArrayType) {
2473
- throw new Error('Unrecognized array type.');
2474
- }
2475
- const [nodeSize] = new Uint16Array(data, 2, 1);
2476
- const [numItems] = new Uint32Array(data, 4, 1);
2388
+ varying vec2 uv;
2477
2389
 
2478
- return new KDBush(numItems, nodeSize, ArrayType, data);
2479
- }
2390
+ void main () {
2391
+ gl_FragColor = texture2D(texture, uv);
2392
+ }
2393
+ `;
2480
2394
 
2481
- /**
2482
- * Creates an index that will hold a given number of items.
2483
- * @param {number} numItems
2484
- * @param {number} [nodeSize=64] Size of the KD-tree node (64 by default).
2485
- * @param {TypedArrayConstructor} [ArrayType=Float64Array] The array type used for coordinates storage (`Float64Array` by default).
2486
- * @param {ArrayBuffer} [data] (For internal use only)
2487
- */
2488
- constructor(numItems, nodeSize = 64, ArrayType = Float64Array, data) {
2489
- if (isNaN(numItems) || numItems < 0)
2490
- throw new Error(`Unexpected numItems value: ${numItems}.`);
2395
+ const VERTEX_SHADER = `
2396
+ precision mediump float;
2491
2397
 
2492
- this.numItems = +numItems;
2493
- this.nodeSize = Math.min(Math.max(+nodeSize, 2), 65535);
2494
- this.ArrayType = ArrayType;
2495
- this.IndexArrayType = numItems < 65536 ? Uint16Array : Uint32Array;
2398
+ uniform mat4 modelViewProjection;
2496
2399
 
2497
- const arrayTypeIndex = ARRAY_TYPES.indexOf(this.ArrayType);
2498
- const coordsByteSize = numItems * 2 * this.ArrayType.BYTES_PER_ELEMENT;
2499
- const idsByteSize = numItems * this.IndexArrayType.BYTES_PER_ELEMENT;
2500
- const padCoords = (8 - (idsByteSize % 8)) % 8;
2400
+ attribute vec2 position;
2501
2401
 
2502
- if (arrayTypeIndex < 0) {
2503
- throw new Error(`Unexpected typed array class: ${ArrayType}.`);
2504
- }
2402
+ varying vec2 uv;
2505
2403
 
2506
- if (data && data instanceof ArrayBuffer) {
2507
- // reconstruct an index from a buffer
2508
- this.data = data;
2509
- this.ids = new this.IndexArrayType(this.data, HEADER_SIZE, numItems);
2510
- this.coords = new this.ArrayType(
2511
- this.data,
2512
- HEADER_SIZE + idsByteSize + padCoords,
2513
- numItems * 2
2514
- );
2515
- this._pos = numItems * 2;
2516
- this._finished = true;
2517
- } else {
2518
- // initialize a new index
2519
- this.data = new ArrayBuffer(
2520
- HEADER_SIZE + coordsByteSize + idsByteSize + padCoords
2521
- );
2522
- this.ids = new this.IndexArrayType(this.data, HEADER_SIZE, numItems);
2523
- this.coords = new this.ArrayType(
2524
- this.data,
2525
- HEADER_SIZE + idsByteSize + padCoords,
2526
- numItems * 2
2527
- );
2528
- this._pos = 0;
2529
- this._finished = false;
2404
+ void main () {
2405
+ uv = position;
2406
+ gl_Position = modelViewProjection * vec4(-1.0 + 2.0 * uv.x, 1.0 - 2.0 * uv.y, 0, 1);
2407
+ }
2408
+ `;
2530
2409
 
2531
- // set header
2532
- new Uint8Array(this.data, 0, 2).set([
2533
- 0xdb,
2534
- (VERSION << 4) + arrayTypeIndex,
2535
- ]);
2536
- new Uint16Array(this.data, 2, 1)[0] = nodeSize;
2537
- new Uint32Array(this.data, 4, 1)[0] = numItems;
2538
- }
2539
- }
2410
+ const AUTO = 'auto';
2540
2411
 
2541
- /**
2542
- * Add a point to the index.
2543
- * @param {number} x
2412
+ const COLOR_NORMAL_IDX = 0;
2413
+ const COLOR_ACTIVE_IDX = 1;
2414
+ const COLOR_HOVER_IDX = 2;
2415
+ const COLOR_BG_IDX = 3;
2416
+ const COLOR_NUM_STATES = 4;
2417
+ const FLOAT_BYTES = Float32Array.BYTES_PER_ELEMENT;
2418
+ const GL_EXTENSIONS = [
2419
+ 'OES_texture_float',
2420
+ 'OES_element_index_uint',
2421
+ 'WEBGL_color_buffer_float',
2422
+ 'EXT_float_blend',
2423
+ ];
2424
+ const CLEAR_OPTIONS = {
2425
+ color: [0, 0, 0, 0], // Transparent background color
2426
+ depth: 1,
2427
+ };
2428
+
2429
+ const MOUSE_MODE_PANZOOM = 'panZoom';
2430
+ const MOUSE_MODE_LASSO = 'lasso';
2431
+ const MOUSE_MODE_ROTATE = 'rotate';
2432
+ const MOUSE_MODES = [
2433
+ MOUSE_MODE_PANZOOM,
2434
+ MOUSE_MODE_LASSO,
2435
+ MOUSE_MODE_ROTATE,
2436
+ ];
2437
+ const DEFAULT_MOUSE_MODE = MOUSE_MODE_PANZOOM;
2438
+
2439
+ // Easing
2440
+ const EASING_FNS = {
2441
+ cubicIn,
2442
+ cubicInOut,
2443
+ cubicOut,
2444
+ linear,
2445
+ quadIn,
2446
+ quadInOut,
2447
+ quadOut,
2448
+ };
2449
+ const DEFAULT_EASING = cubicInOut;
2450
+
2451
+ const CONTINUOUS = 'continuous';
2452
+ const CATEGORICAL = 'categorical';
2453
+ const VALUE_ZW_DATA_TYPES = [CONTINUOUS, CATEGORICAL];
2454
+
2455
+ // Default lasso
2456
+ const LASSO_CLEAR_ON_DESELECT = 'deselect';
2457
+ const LASSO_CLEAR_ON_END = 'lassoEnd';
2458
+ const LASSO_CLEAR_EVENTS = [LASSO_CLEAR_ON_DESELECT, LASSO_CLEAR_ON_END];
2459
+ const LASSO_BRUSH_MIN_MIN_DIST = 3;
2460
+ const DEFAULT_LASSO_COLOR = [0, 0.666666667, 1, 1];
2461
+ const DEFAULT_LASSO_LINE_WIDTH = 2;
2462
+ const DEFAULT_LASSO_INITIATOR = false;
2463
+ const DEFAULT_LASSO_MIN_DELAY$1 = 10;
2464
+ const DEFAULT_LASSO_MIN_DIST$1 = 3;
2465
+ const DEFAULT_LASSO_CLEAR_EVENT = LASSO_CLEAR_ON_END;
2466
+ const DEFAULT_LASSO_ON_LONG_PRESS = false;
2467
+ const DEFAULT_LASSO_LONG_PRESS_TIME = 750;
2468
+ const DEFAULT_LASSO_LONG_PRESS_AFTER_EFFECT_TIME = 500;
2469
+ const DEFAULT_LASSO_LONG_PRESS_EFFECT_DELAY = 100;
2470
+ const DEFAULT_LASSO_LONG_PRESS_REVERT_EFFECT_TIME = 250;
2471
+ const DEFAULT_LASSO_BRUSH_SIZE = 24;
2472
+ const DEFAULT_LASSO_MODE = null;
2473
+
2474
+ // Key mapping
2475
+ const KEY_ACTION_INTERSECT = 'intersect';
2476
+ const KEY_ACTION_LASSO = 'lasso';
2477
+ const KEY_ACTION_ROTATE = 'rotate';
2478
+ const KEY_ACTION_MERGE = 'merge';
2479
+ const KEY_ACTION_REMOVE = 'remove';
2480
+ const KEY_ACTIONS = [
2481
+ KEY_ACTION_LASSO,
2482
+ KEY_ACTION_ROTATE,
2483
+ KEY_ACTION_INTERSECT,
2484
+ KEY_ACTION_MERGE,
2485
+ KEY_ACTION_REMOVE,
2486
+ ];
2487
+ const KEY_ALT = 'alt';
2488
+ const KEY_CMD = 'cmd';
2489
+ const KEY_CTRL = 'ctrl';
2490
+ const KEY_META = 'meta';
2491
+ const KEY_SHIFT = 'shift';
2492
+ const KEYS = [KEY_ALT, KEY_CMD, KEY_CTRL, KEY_META, KEY_SHIFT];
2493
+ const DEFAULT_ACTION_KEY_MAP = {
2494
+ [KEY_ACTION_REMOVE]: KEY_ALT,
2495
+ [KEY_ACTION_ROTATE]: KEY_ALT,
2496
+ [KEY_ACTION_LASSO]: KEY_SHIFT,
2497
+ [KEY_ACTION_MERGE]: KEY_CMD,
2498
+ };
2499
+
2500
+ // Default attribute
2501
+ const DEFAULT_DATA_ASPECT_RATIO = 1;
2502
+ const DEFAULT_WIDTH = AUTO;
2503
+ const DEFAULT_HEIGHT = AUTO;
2504
+ const DEFAULT_GAMMA = 1;
2505
+
2506
+ // Default styles
2507
+ const MIN_POINT_SIZE = 1;
2508
+ const DEFAULT_POINT_SCALE_MODE = 'asinh';
2509
+ const DEFAULT_POINT_SIZE = 6;
2510
+ const DEFAULT_POINT_SIZE_SELECTED = 2;
2511
+ const DEFAULT_POINT_OUTLINE_WIDTH = 2;
2512
+ const DEFAULT_SIZE_BY = null;
2513
+ const DEFAULT_POINT_ORDER = null;
2514
+ const DEFAULT_POINT_CONNECTION_SIZE = 2;
2515
+ const DEFAULT_POINT_CONNECTION_SIZE_ACTIVE = 2;
2516
+ const DEFAULT_POINT_CONNECTION_SIZE_BY = null;
2517
+ const DEFAULT_POINT_CONNECTION_OPACITY = null;
2518
+ const DEFAULT_POINT_CONNECTION_OPACITY_BY = null;
2519
+ const DEFAULT_POINT_CONNECTION_OPACITY_ACTIVE = 0.66;
2520
+ const DEFAULT_OPACITY = 1;
2521
+ const DEFAULT_OPACITY_BY = null;
2522
+ const DEFAULT_OPACITY_BY_DENSITY_FILL = 0.15;
2523
+ const DEFAULT_OPACITY_BY_DENSITY_DEBOUNCE_TIME = 25;
2524
+ const DEFAULT_OPACITY_INACTIVE_MAX = 1;
2525
+ const DEFAULT_OPACITY_INACTIVE_SCALE = 1;
2526
+ const DEFAULT_COLOR_BY = null;
2527
+ const DEFAULT_COLOR_NORMAL = [0.66, 0.66, 0.66, DEFAULT_OPACITY];
2528
+ const DEFAULT_COLOR_ACTIVE = [0, 0.55, 1, 1];
2529
+ const DEFAULT_COLOR_HOVER = [1, 1, 1, 1];
2530
+ const DEFAULT_COLOR_BG = [0, 0, 0, 1];
2531
+ const DEFAULT_POINT_CONNECTION_COLOR_BY = null;
2532
+ const DEFAULT_POINT_CONNECTION_COLOR_NORMAL = [0.66, 0.66, 0.66, 0.2];
2533
+ const DEFAULT_POINT_CONNECTION_COLOR_ACTIVE = [0, 0.55, 1, 1];
2534
+ const DEFAULT_POINT_CONNECTION_COLOR_HOVER = [1, 1, 1, 1];
2535
+
2536
+ // Annotations
2537
+ const DEFAULT_ANNOTATION_LINE_COLOR = [1, 1, 1, 0.5];
2538
+ const DEFAULT_ANNOTATION_LINE_WIDTH = 1;
2539
+ const DEFAULT_ANNOTATION_HVLINE_LIMIT = 1000;
2540
+
2541
+ // Default view
2542
+ const DEFAULT_TARGET = [0, 0];
2543
+ const DEFAULT_DISTANCE = 1;
2544
+ const DEFAULT_ROTATION = 0;
2545
+ // biome-ignore format: the array should not be formatted
2546
+ const DEFAULT_VIEW = new Float32Array([
2547
+ 1, 0, 0, 0,
2548
+ 0, 1, 0, 0,
2549
+ 0, 0, 1, 0,
2550
+ 0, 0, 0, 1,
2551
+ ]);
2552
+
2553
+ // Error codes
2554
+ const IMAGE_LOAD_ERROR = 'IMAGE_LOAD_ERROR';
2555
+
2556
+ // Default misc
2557
+ const DEFAULT_BACKGROUND_IMAGE = null;
2558
+ const DEFAULT_SHOW_RETICLE = false;
2559
+ const DEFAULT_RETICLE_COLOR = [1, 1, 1, 0.5];
2560
+ const DEFAULT_DESELECT_ON_DBL_CLICK = true;
2561
+ const DEFAULT_DESELECT_ON_ESCAPE = true;
2562
+ const DEFAULT_SHOW_POINT_CONNECTIONS = false;
2563
+ const DEFAULT_POINT_CONNECTION_MAX_INT_POINTS_PER_SEGMENT = 100;
2564
+ const DEFAULT_POINT_CONNECTION_INT_POINTS_TOLERANCE = 1 / 500;
2565
+ const DEFAULT_POINT_SIZE_MOUSE_DETECTION = 'auto';
2566
+ const DEFAULT_PERFORMANCE_MODE = false;
2567
+ const SINGLE_CLICK_DELAY = 200;
2568
+ const LONG_CLICK_TIME = 500;
2569
+ const Z_NAMES = new Set(['z', 'valueZ', 'valueA', 'value1', 'category']);
2570
+ const W_NAMES = new Set(['w', 'valueW', 'valueB', 'value2', 'value']);
2571
+ const DEFAULT_IMAGE_LOAD_TIMEOUT = 15000;
2572
+ const DEFAULT_SPATIAL_INDEX_USE_WORKER = undefined;
2573
+ const DEFAULT_CAMERA_IS_FIXED = false;
2574
+ const DEFAULT_ANTI_ALIASING = 0.5;
2575
+ const DEFAULT_PIXEL_ALIGNED = false;
2576
+ const DEFAULT_LASSO_TYPE$1 = 'lasso';
2577
+ const SKIP_DEPRECATION_VALUE_TRANSLATION = Symbol(
2578
+ 'SKIP_DEPRECATION_VALUE_TRANSLATION',
2579
+ );
2580
+
2581
+ // Error messages
2582
+ const ERROR_POINTS_NOT_DRAWN = 'Points have not been drawn';
2583
+ const ERROR_INSTANCE_IS_DESTROYED = 'The instance was already destroyed';
2584
+ const ERROR_IS_DRAWING =
2585
+ 'Ignoring draw call as the previous draw call has not yet finished. To avoid this warning `await` the draw call.';
2586
+
2587
+ /**
2588
+ * KDBush - A fast static index for 2D points
2589
+ * @license ISC License
2590
+ * @copyright Vladimir Agafonkin 2018
2591
+ * @version 4.0.2
2592
+ * @see https://github.com/mourner/kdbush/
2593
+ */
2594
+ var createKDBushClass = () => {
2595
+ const ARRAY_TYPES = [
2596
+ Int8Array,
2597
+ Uint8Array,
2598
+ Uint8ClampedArray,
2599
+ Int16Array,
2600
+ Uint16Array,
2601
+ Int32Array,
2602
+ Uint32Array,
2603
+ Float32Array,
2604
+ Float64Array,
2605
+ ];
2606
+
2607
+ /** @typedef {Int8ArrayConstructor | Uint8ArrayConstructor | Uint8ClampedArrayConstructor | Int16ArrayConstructor | Uint16ArrayConstructor | Int32ArrayConstructor | Uint32ArrayConstructor | Float32ArrayConstructor | Float64ArrayConstructor} TypedArrayConstructor */
2608
+
2609
+ const VERSION = 1; // serialized format version
2610
+ const HEADER_SIZE = 8;
2611
+
2612
+ class KDBush {
2613
+ /**
2614
+ * Creates an index from raw `ArrayBuffer` data.
2615
+ * @param {ArrayBuffer} data
2616
+ */
2617
+ static from(data) {
2618
+ if (!(data instanceof ArrayBuffer)) {
2619
+ throw new Error('Data must be an instance of ArrayBuffer.');
2620
+ }
2621
+ const [magic, versionAndType] = new Uint8Array(data, 0, 2);
2622
+ if (magic !== 0xdb) {
2623
+ throw new Error('Data does not appear to be in a KDBush format.');
2624
+ }
2625
+ const version = versionAndType >> 4;
2626
+ if (version !== VERSION) {
2627
+ throw new Error(`Got v${version} data when expected v${VERSION}.`);
2628
+ }
2629
+ const ArrayType = ARRAY_TYPES[versionAndType & 0x0f];
2630
+ if (!ArrayType) {
2631
+ throw new Error('Unrecognized array type.');
2632
+ }
2633
+ const [nodeSize] = new Uint16Array(data, 2, 1);
2634
+ const [numItems] = new Uint32Array(data, 4, 1);
2635
+
2636
+ return new KDBush(numItems, nodeSize, ArrayType, data);
2637
+ }
2638
+
2639
+ /**
2640
+ * Creates an index that will hold a given number of items.
2641
+ * @param {number} numItems
2642
+ * @param {number} [nodeSize=64] Size of the KD-tree node (64 by default).
2643
+ * @param {TypedArrayConstructor} [ArrayType=Float64Array] The array type used for coordinates storage (`Float64Array` by default).
2644
+ * @param {ArrayBuffer} [data] (For internal use only)
2645
+ */
2646
+ constructor(numItems, nodeSize = 64, ArrayType = Float64Array, data) {
2647
+ if (isNaN(numItems) || numItems < 0)
2648
+ throw new Error(`Unexpected numItems value: ${numItems}.`);
2649
+
2650
+ this.numItems = +numItems;
2651
+ this.nodeSize = Math.min(Math.max(+nodeSize, 2), 65535);
2652
+ this.ArrayType = ArrayType;
2653
+ this.IndexArrayType = numItems < 65536 ? Uint16Array : Uint32Array;
2654
+
2655
+ const arrayTypeIndex = ARRAY_TYPES.indexOf(this.ArrayType);
2656
+ const coordsByteSize = numItems * 2 * this.ArrayType.BYTES_PER_ELEMENT;
2657
+ const idsByteSize = numItems * this.IndexArrayType.BYTES_PER_ELEMENT;
2658
+ const padCoords = (8 - (idsByteSize % 8)) % 8;
2659
+
2660
+ if (arrayTypeIndex < 0) {
2661
+ throw new Error(`Unexpected typed array class: ${ArrayType}.`);
2662
+ }
2663
+
2664
+ if (data && data instanceof ArrayBuffer) {
2665
+ // reconstruct an index from a buffer
2666
+ this.data = data;
2667
+ this.ids = new this.IndexArrayType(this.data, HEADER_SIZE, numItems);
2668
+ this.coords = new this.ArrayType(
2669
+ this.data,
2670
+ HEADER_SIZE + idsByteSize + padCoords,
2671
+ numItems * 2
2672
+ );
2673
+ this._pos = numItems * 2;
2674
+ this._finished = true;
2675
+ } else {
2676
+ // initialize a new index
2677
+ this.data = new ArrayBuffer(
2678
+ HEADER_SIZE + coordsByteSize + idsByteSize + padCoords
2679
+ );
2680
+ this.ids = new this.IndexArrayType(this.data, HEADER_SIZE, numItems);
2681
+ this.coords = new this.ArrayType(
2682
+ this.data,
2683
+ HEADER_SIZE + idsByteSize + padCoords,
2684
+ numItems * 2
2685
+ );
2686
+ this._pos = 0;
2687
+ this._finished = false;
2688
+
2689
+ // set header
2690
+ new Uint8Array(this.data, 0, 2).set([
2691
+ 0xdb,
2692
+ (VERSION << 4) + arrayTypeIndex,
2693
+ ]);
2694
+ new Uint16Array(this.data, 2, 1)[0] = nodeSize;
2695
+ new Uint32Array(this.data, 4, 1)[0] = numItems;
2696
+ }
2697
+ }
2698
+
2699
+ /**
2700
+ * Add a point to the index.
2701
+ * @param {number} x
2544
2702
  * @param {number} y
2545
2703
  * @returns {number} An incremental index associated with the added item (starting from `0`).
2546
2704
  */
@@ -2804,7 +2962,6 @@ var workerFn = () => {
2804
2962
  self.postMessage({ error: new Error('Invalid point data') });
2805
2963
  }
2806
2964
 
2807
- // biome-ignore lint/correctness/noUndeclaredVariables: KDBush is made available during compilation
2808
2965
  const index = new KDBush(points.length, event.data.nodeSize);
2809
2966
 
2810
2967
  for (const [x, y] of points) {
@@ -2817,9 +2974,6 @@ var workerFn = () => {
2817
2974
  });
2818
2975
  };
2819
2976
 
2820
- // biome-ignore lint/style/useNamingConvention: KDBush is a library name
2821
-
2822
- // biome-ignore lint/style/useNamingConvention: KDBush is a library name
2823
2977
  const KDBush$1 = createKDBushClass();
2824
2978
  const WORKER_THRESHOLD = 1000000;
2825
2979
 
@@ -2827,7 +2981,6 @@ const createWorker = (fn) => {
2827
2981
  const kdbushStr = createKDBushClass.toString();
2828
2982
  const fnStr = fn.toString();
2829
2983
  const workerStr =
2830
- // biome-ignore lint/style/useTemplate: Prefer one assignment per line
2831
2984
  `const createKDBushClass = ${kdbushStr};` +
2832
2985
  'KDBush = createKDBushClass();' +
2833
2986
  `const createWorker = ${fnStr};` +
@@ -2883,190 +3036,16 @@ const createKdbush = (
2883
3036
  }
2884
3037
  });
2885
3038
 
3039
+ const kdbushFrom = (buffer) => KDBush$1.from(buffer);
3040
+
2886
3041
  const DEFAULT_LASSO_START_INITIATOR_SHOW = true;
2887
- const DEFAULT_LASSO_MIN_DELAY$1 = 8;
2888
- const DEFAULT_LASSO_MIN_DIST$1 = 2;
2889
- const DEFAULT_LASSO_TYPE$1 = 'freeform';
3042
+ const DEFAULT_LASSO_MIN_DELAY = 8;
3043
+ const DEFAULT_LASSO_MIN_DIST = 2;
3044
+ const DEFAULT_LASSO_TYPE = 'freeform';
2890
3045
  const DEFAULT_BRUSH_SIZE = 24;
2891
3046
  const LASSO_SHOW_START_INITIATOR_TIME = 2500;
2892
3047
  const LASSO_HIDE_START_INITIATOR_TIME = 250;
2893
3048
 
2894
- const AUTO = 'auto';
2895
-
2896
- const COLOR_NORMAL_IDX = 0;
2897
- const COLOR_ACTIVE_IDX = 1;
2898
- const COLOR_HOVER_IDX = 2;
2899
- const COLOR_BG_IDX = 3;
2900
- const COLOR_NUM_STATES = 4;
2901
- const FLOAT_BYTES = Float32Array.BYTES_PER_ELEMENT;
2902
- const GL_EXTENSIONS = [
2903
- 'OES_texture_float',
2904
- 'OES_element_index_uint',
2905
- 'WEBGL_color_buffer_float',
2906
- 'EXT_float_blend',
2907
- ];
2908
- const CLEAR_OPTIONS = {
2909
- color: [0, 0, 0, 0], // Transparent background color
2910
- depth: 1,
2911
- };
2912
-
2913
- const MOUSE_MODE_PANZOOM = 'panZoom';
2914
- const MOUSE_MODE_LASSO = 'lasso';
2915
- const MOUSE_MODE_ROTATE = 'rotate';
2916
- const MOUSE_MODES = [
2917
- MOUSE_MODE_PANZOOM,
2918
- MOUSE_MODE_LASSO,
2919
- MOUSE_MODE_ROTATE,
2920
- ];
2921
- const DEFAULT_MOUSE_MODE = MOUSE_MODE_PANZOOM;
2922
-
2923
- // Easing
2924
- const EASING_FNS = {
2925
- cubicIn,
2926
- cubicInOut,
2927
- cubicOut,
2928
- linear,
2929
- quadIn,
2930
- quadInOut,
2931
- quadOut,
2932
- };
2933
- const DEFAULT_EASING = cubicInOut;
2934
-
2935
- const CONTINUOUS = 'continuous';
2936
- const CATEGORICAL = 'categorical';
2937
- const VALUE_ZW_DATA_TYPES = [CONTINUOUS, CATEGORICAL];
2938
-
2939
- // Default lasso
2940
- const LASSO_CLEAR_ON_DESELECT = 'deselect';
2941
- const LASSO_CLEAR_ON_END = 'lassoEnd';
2942
- const LASSO_CLEAR_EVENTS = [LASSO_CLEAR_ON_DESELECT, LASSO_CLEAR_ON_END];
2943
- const LASSO_BRUSH_MIN_MIN_DIST = 3;
2944
- const DEFAULT_LASSO_COLOR = [0, 0.666666667, 1, 1];
2945
- const DEFAULT_LASSO_LINE_WIDTH = 2;
2946
- const DEFAULT_LASSO_INITIATOR = false;
2947
- const DEFAULT_LASSO_MIN_DELAY = 10;
2948
- const DEFAULT_LASSO_MIN_DIST = 3;
2949
- const DEFAULT_LASSO_CLEAR_EVENT = LASSO_CLEAR_ON_END;
2950
- const DEFAULT_LASSO_ON_LONG_PRESS = false;
2951
- const DEFAULT_LASSO_LONG_PRESS_TIME = 750;
2952
- const DEFAULT_LASSO_LONG_PRESS_AFTER_EFFECT_TIME = 500;
2953
- const DEFAULT_LASSO_LONG_PRESS_EFFECT_DELAY = 100;
2954
- const DEFAULT_LASSO_LONG_PRESS_REVERT_EFFECT_TIME = 250;
2955
- const DEFAULT_LASSO_BRUSH_SIZE = 24;
2956
- const DEFAULT_LASSO_MODE = null;
2957
-
2958
- // Key mapping
2959
- const KEY_ACTION_INTERSECT = 'intersect';
2960
- const KEY_ACTION_LASSO = 'lasso';
2961
- const KEY_ACTION_ROTATE = 'rotate';
2962
- const KEY_ACTION_MERGE = 'merge';
2963
- const KEY_ACTION_REMOVE = 'remove';
2964
- const KEY_ACTIONS = [
2965
- KEY_ACTION_LASSO,
2966
- KEY_ACTION_ROTATE,
2967
- KEY_ACTION_INTERSECT,
2968
- KEY_ACTION_MERGE,
2969
- KEY_ACTION_REMOVE,
2970
- ];
2971
- const KEY_ALT = 'alt';
2972
- const KEY_CMD = 'cmd';
2973
- const KEY_CTRL = 'ctrl';
2974
- const KEY_META = 'meta';
2975
- const KEY_SHIFT = 'shift';
2976
- const KEYS = [KEY_ALT, KEY_CMD, KEY_CTRL, KEY_META, KEY_SHIFT];
2977
- const DEFAULT_ACTION_KEY_MAP = {
2978
- [KEY_ACTION_REMOVE]: KEY_ALT,
2979
- [KEY_ACTION_ROTATE]: KEY_ALT,
2980
- [KEY_ACTION_LASSO]: KEY_SHIFT,
2981
- [KEY_ACTION_MERGE]: KEY_CMD,
2982
- };
2983
-
2984
- // Default attribute
2985
- const DEFAULT_DATA_ASPECT_RATIO = 1;
2986
- const DEFAULT_WIDTH = AUTO;
2987
- const DEFAULT_HEIGHT = AUTO;
2988
- const DEFAULT_GAMMA = 1;
2989
-
2990
- // Default styles
2991
- const MIN_POINT_SIZE = 1;
2992
- const DEFAULT_POINT_SCALE_MODE = 'asinh';
2993
- const DEFAULT_POINT_SIZE = 6;
2994
- const DEFAULT_POINT_SIZE_SELECTED = 2;
2995
- const DEFAULT_POINT_OUTLINE_WIDTH = 2;
2996
- const DEFAULT_SIZE_BY = null;
2997
- const DEFAULT_POINT_CONNECTION_SIZE = 2;
2998
- const DEFAULT_POINT_CONNECTION_SIZE_ACTIVE = 2;
2999
- const DEFAULT_POINT_CONNECTION_SIZE_BY = null;
3000
- const DEFAULT_POINT_CONNECTION_OPACITY = null;
3001
- const DEFAULT_POINT_CONNECTION_OPACITY_BY = null;
3002
- const DEFAULT_POINT_CONNECTION_OPACITY_ACTIVE = 0.66;
3003
- const DEFAULT_OPACITY = 1;
3004
- const DEFAULT_OPACITY_BY = null;
3005
- const DEFAULT_OPACITY_BY_DENSITY_FILL = 0.15;
3006
- const DEFAULT_OPACITY_BY_DENSITY_DEBOUNCE_TIME = 25;
3007
- const DEFAULT_OPACITY_INACTIVE_MAX = 1;
3008
- const DEFAULT_OPACITY_INACTIVE_SCALE = 1;
3009
- const DEFAULT_COLOR_BY = null;
3010
- const DEFAULT_COLOR_NORMAL = [0.66, 0.66, 0.66, DEFAULT_OPACITY];
3011
- const DEFAULT_COLOR_ACTIVE = [0, 0.55, 1, 1];
3012
- const DEFAULT_COLOR_HOVER = [1, 1, 1, 1];
3013
- const DEFAULT_COLOR_BG = [0, 0, 0, 1];
3014
- const DEFAULT_POINT_CONNECTION_COLOR_BY = null;
3015
- const DEFAULT_POINT_CONNECTION_COLOR_NORMAL = [0.66, 0.66, 0.66, 0.2];
3016
- const DEFAULT_POINT_CONNECTION_COLOR_ACTIVE = [0, 0.55, 1, 1];
3017
- const DEFAULT_POINT_CONNECTION_COLOR_HOVER = [1, 1, 1, 1];
3018
-
3019
- // Annotations
3020
- const DEFAULT_ANNOTATION_LINE_COLOR = [1, 1, 1, 0.5];
3021
- const DEFAULT_ANNOTATION_LINE_WIDTH = 1;
3022
- const DEFAULT_ANNOTATION_HVLINE_LIMIT = 1000;
3023
-
3024
- // Default view
3025
- const DEFAULT_TARGET = [0, 0];
3026
- const DEFAULT_DISTANCE = 1;
3027
- const DEFAULT_ROTATION = 0;
3028
- // biome-ignore format: the array should not be formatted
3029
- const DEFAULT_VIEW = new Float32Array([
3030
- 1, 0, 0, 0,
3031
- 0, 1, 0, 0,
3032
- 0, 0, 1, 0,
3033
- 0, 0, 0, 1,
3034
- ]);
3035
-
3036
- // Error codes
3037
- const IMAGE_LOAD_ERROR = 'IMAGE_LOAD_ERROR';
3038
-
3039
- // Default misc
3040
- const DEFAULT_BACKGROUND_IMAGE = null;
3041
- const DEFAULT_SHOW_RETICLE = false;
3042
- const DEFAULT_RETICLE_COLOR = [1, 1, 1, 0.5];
3043
- const DEFAULT_DESELECT_ON_DBL_CLICK = true;
3044
- const DEFAULT_DESELECT_ON_ESCAPE = true;
3045
- const DEFAULT_SHOW_POINT_CONNECTIONS = false;
3046
- const DEFAULT_POINT_CONNECTION_MAX_INT_POINTS_PER_SEGMENT = 100;
3047
- const DEFAULT_POINT_CONNECTION_INT_POINTS_TOLERANCE = 1 / 500;
3048
- const DEFAULT_POINT_SIZE_MOUSE_DETECTION = 'auto';
3049
- const DEFAULT_PERFORMANCE_MODE = false;
3050
- const SINGLE_CLICK_DELAY = 200;
3051
- const LONG_CLICK_TIME = 500;
3052
- const Z_NAMES = new Set(['z', 'valueZ', 'valueA', 'value1', 'category']);
3053
- const W_NAMES = new Set(['w', 'valueW', 'valueB', 'value2', 'value']);
3054
- const DEFAULT_IMAGE_LOAD_TIMEOUT = 15000;
3055
- const DEFAULT_SPATIAL_INDEX_USE_WORKER = undefined;
3056
- const DEFAULT_CAMERA_IS_FIXED = false;
3057
- const DEFAULT_ANTI_ALIASING = 0.5;
3058
- const DEFAULT_PIXEL_ALIGNED = false;
3059
- const DEFAULT_LASSO_TYPE = 'lasso';
3060
- const SKIP_DEPRECATION_VALUE_TRANSLATION = Symbol(
3061
- 'SKIP_DEPRECATION_VALUE_TRANSLATION',
3062
- );
3063
-
3064
- // Error messages
3065
- const ERROR_POINTS_NOT_DRAWN = 'Points have not been drawn';
3066
- const ERROR_INSTANCE_IS_DESTROYED = 'The instance was already destroyed';
3067
- const ERROR_IS_DRAWING =
3068
- 'Ignoring draw call as the previous draw call has not yet finished. To avoid this warning `await` the draw call.';
3069
-
3070
3049
  const getInTime = (p, time, extraTime) => (1 - p) * time + extraTime;
3071
3050
 
3072
3051
  const getMainInAnimation = (t, d) =>
@@ -3497,10 +3476,10 @@ const createLasso = (
3497
3476
  initiatorParentElement: initialInitiatorParentElement = document.body,
3498
3477
  longPressIndicatorParentElement:
3499
3478
  initialLongPressIndicatorParentElement = document.body,
3500
- minDelay: initialMinDelay = DEFAULT_LASSO_MIN_DELAY$1,
3501
- minDist: initialMinDist = DEFAULT_LASSO_MIN_DIST$1,
3479
+ minDelay: initialMinDelay = DEFAULT_LASSO_MIN_DELAY,
3480
+ minDist: initialMinDist = DEFAULT_LASSO_MIN_DIST,
3502
3481
  pointNorm: initialPointNorm = identity,
3503
- type: initialType = DEFAULT_LASSO_TYPE$1,
3482
+ type: initialType = DEFAULT_LASSO_TYPE,
3504
3483
  brushSize: initialBrushSize = DEFAULT_BRUSH_SIZE,
3505
3484
  } = {},
3506
3485
  ) => {
@@ -4150,1052 +4129,1041 @@ const createLasso = (
4150
4129
  });
4151
4130
 
4152
4131
  initiatorParentElement.appendChild(initiator);
4153
- longPressIndicatorParentElement.appendChild(longPress);
4154
-
4155
- set({
4156
- onDraw,
4157
- onStart,
4158
- onEnd,
4159
- enableInitiator,
4160
- initiatorParentElement,
4161
- type,
4162
- brushSize,
4163
- });
4164
-
4165
- return pipe(
4166
- withStaticProperty('initiator', initiator),
4167
- withStaticProperty('longPressIndicator', longPress),
4168
- withPublicMethods(),
4169
- withConstructor(createLasso),
4170
- )({});
4171
- };
4172
-
4173
- /**
4174
- * Check if all GL extensions are supported and enabled and warn otherwise
4175
- * @param {import('regl').Regl} regl Regl instance to be tested
4176
- * @param {boolean} silent If `true` the function will not print `console.warn` statements
4177
- * @return {boolean} If `true` all required GL extensions are supported
4178
- */
4179
- const checkReglExtensions = (regl, silent) => {
4180
- if (!regl) {
4181
- return false;
4182
- }
4183
- return GL_EXTENSIONS.reduce((every, extension) => {
4184
- if (!regl.hasExtension(extension)) {
4185
- if (!silent) {
4186
- // biome-ignore lint/suspicious/noConsole: This is a legitimately useful warning
4187
- console.warn(
4188
- `WebGL: ${extension} extension not supported. Scatterplot might not render properly`,
4189
- );
4190
- }
4191
- return false;
4192
- }
4193
- return every;
4194
- }, true);
4195
- };
4132
+ longPressIndicatorParentElement.appendChild(longPress);
4196
4133
 
4197
- /**
4198
- * Create a new Regl instance with `GL_EXTENSIONS` enables
4199
- * @param {HTMLCanvasElement} canvas Canvas element to be rendered on
4200
- * @return {import('regl').Regl} New Regl instance
4201
- */
4202
- const createRegl = (canvas) => {
4203
- const gl = canvas.getContext('webgl', {
4204
- antialias: true,
4205
- preserveDrawingBuffer: true,
4134
+ set({
4135
+ onDraw,
4136
+ onStart,
4137
+ onEnd,
4138
+ enableInitiator,
4139
+ initiatorParentElement,
4140
+ type,
4141
+ brushSize,
4206
4142
  });
4207
- const extensions = [];
4208
-
4209
- // Needed to run the tests properly as the headless-gl doesn't support all
4210
- // extensions, which is fine for the functional tests.
4211
- for (const extension of GL_EXTENSIONS) {
4212
- if (gl.getExtension(extension)) {
4213
- extensions.push(extension);
4214
- } else {
4215
- // biome-ignore lint/suspicious/noConsole: This is a legitimately useful warning
4216
- console.warn(
4217
- `WebGL: ${extension} extension not supported. Scatterplot might not render properly`,
4218
- );
4219
- }
4220
- }
4221
4143
 
4222
- return createOriginalRegl({ gl, extensions });
4144
+ return pipe(
4145
+ withStaticProperty('initiator', initiator),
4146
+ withStaticProperty('longPressIndicator', longPress),
4147
+ withPublicMethods(),
4148
+ withConstructor(createLasso),
4149
+ )({});
4223
4150
  };
4224
4151
 
4225
- /**
4226
- * L2 distance between a pair of 2D points
4227
- * @param {number} x1 X coordinate of the first point
4228
- * @param {number} y1 Y coordinate of the first point
4229
- * @param {number} x2 X coordinate of the second point
4230
- * @param {number} y2 Y coordinate of the first point
4231
- * @return {number} L2 distance
4232
- */
4233
- const dist = (x1, y1, x2, y2) =>
4234
- Math.sqrt((x1 - x2) ** 2 + (y1 - y2) ** 2);
4152
+ const FRAGMENT_SHADER$1 = `
4153
+ precision highp float;
4235
4154
 
4236
- /**
4237
- * Get the bounding box of a set of 2D positions
4238
- * @param {array} positions2d 2D positions to be checked
4239
- * @return {array} Quadruple of form `[xMin, yMin, xMax, yMax]` defining the
4240
- * bounding box
4241
- */
4242
- // biome-ignore lint/style/useNamingConvention: BBox stands for BoundingBox
4243
- const getBBox = (positions2d) => {
4244
- let xMin = Number.POSITIVE_INFINITY;
4245
- let xMax = Number.NEGATIVE_INFINITY;
4246
- let yMin = Number.POSITIVE_INFINITY;
4247
- let yMax = Number.NEGATIVE_INFINITY;
4155
+ uniform float antiAliasing;
4248
4156
 
4249
- for (let i = 0; i < positions2d.length; i += 2) {
4250
- xMin = positions2d[i] < xMin ? positions2d[i] : xMin;
4251
- xMax = positions2d[i] > xMax ? positions2d[i] : xMax;
4252
- yMin = positions2d[i + 1] < yMin ? positions2d[i + 1] : yMin;
4253
- yMax = positions2d[i + 1] > yMax ? positions2d[i + 1] : yMax;
4254
- }
4157
+ varying vec4 color;
4158
+ varying float finalPointSize;
4255
4159
 
4256
- return [xMin, yMin, xMax, yMax];
4257
- };
4160
+ float linearstep(float edge0, float edge1, float x) {
4161
+ return clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0);
4162
+ }
4258
4163
 
4259
- /**
4260
- * Test whether a bounding box is actually specifying an area
4261
- * @param {array} bBox The bounding box to be checked
4262
- * @return {array} `true` if the bounding box is valid
4263
- */
4264
- // biome-ignore lint/style/useNamingConvention: BBox stands for BoundingBox
4265
- const isValidBBox = ([xMin, yMin, xMax, yMax]) =>
4266
- Number.isFinite(xMin) &&
4267
- Number.isFinite(yMin) &&
4268
- Number.isFinite(xMax) &&
4269
- Number.isFinite(yMax) &&
4270
- xMax - xMin > 0 &&
4271
- yMax - yMin > 0;
4164
+ void main() {
4165
+ vec2 c = gl_PointCoord * 2.0 - 1.0;
4166
+ float sdf = length(c) * finalPointSize;
4167
+ float alpha = linearstep(finalPointSize + antiAliasing, finalPointSize - antiAliasing, sdf);
4272
4168
 
4273
- const REGEX_HEX_TO_RGB = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
4169
+ gl_FragColor = vec4(color.rgb, alpha * color.a);
4170
+ }
4171
+ `;
4274
4172
 
4275
- /**
4276
- * Convert a HEX-encoded color to an RGB-encoded color
4277
- * @param {string} hex HEX-encoded color string.
4278
- * @param {boolean} isNormalize If `true` the returned RGB values will be
4279
- * normalized to `[0,1]`.
4280
- * @return {array} Triple holding the RGB values.
4281
- */
4282
- const hexToRgb = (hex, isNormalize = false) =>
4283
- hex
4284
- .replace(REGEX_HEX_TO_RGB, (_m, r, g, b) => `#${r}${r}${g}${g}${b}${b}`)
4285
- .substring(1)
4286
- .match(/.{2}/g)
4287
- .map((x) => Number.parseInt(x, 16) / 255 ** isNormalize);
4173
+ const createVertexShader = (globalState) => `
4174
+ precision highp float;
4288
4175
 
4289
- const isConditionalArray = (a, condition, { minLength = 0 } = {}) =>
4290
- Array.isArray(a) && a.length >= minLength && a.every(condition);
4176
+ uniform sampler2D colorTex;
4177
+ uniform float colorTexRes;
4178
+ uniform float colorTexEps;
4179
+ uniform sampler2D stateTex;
4180
+ uniform float stateTexRes;
4181
+ uniform float stateTexEps;
4182
+ uniform float devicePixelRatio;
4183
+ uniform sampler2D encodingTex;
4184
+ uniform float encodingTexRes;
4185
+ uniform float encodingTexEps;
4186
+ uniform float pointSizeExtra;
4187
+ uniform float pointOpacityMax;
4188
+ uniform float pointOpacityScale;
4189
+ uniform float numPoints;
4190
+ uniform float globalState;
4191
+ uniform float isColoredByZ;
4192
+ uniform float isColoredByW;
4193
+ uniform float isOpacityByZ;
4194
+ uniform float isOpacityByW;
4195
+ uniform float isOpacityByDensity;
4196
+ uniform float isSizedByZ;
4197
+ uniform float isSizedByW;
4198
+ uniform float isPixelAligned;
4199
+ uniform float colorMultiplicator;
4200
+ uniform float opacityMultiplicator;
4201
+ uniform float opacityDensity;
4202
+ uniform float sizeMultiplicator;
4203
+ uniform float numColorStates;
4204
+ uniform float pointScale;
4205
+ uniform float drawingBufferWidth;
4206
+ uniform float drawingBufferHeight;
4207
+ uniform mat4 modelViewProjection;
4291
4208
 
4292
- const isPositiveNumber = (x) => !Number.isNaN(+x) && +x >= 0;
4209
+ attribute vec2 stateIndex;
4293
4210
 
4294
- const isStrictlyPositiveNumber = (x) => !Number.isNaN(+x) && +x > 0;
4211
+ varying vec4 color;
4212
+ varying float finalPointSize;
4295
4213
 
4296
- /**
4297
- * Create a function to limit choices to a predefined list
4298
- * @param {array} choices Array of acceptable choices
4299
- * @param {*} defaultOption Default choice
4300
- * @return {function} Function limiting the choices
4301
- */
4302
- const limit = (choices, defaultChoice) => (choice) =>
4303
- choices.indexOf(choice) >= 0 ? choice : defaultChoice;
4214
+ void main() {
4215
+ vec4 state = texture2D(stateTex, stateIndex);
4304
4216
 
4305
- /**
4306
- * Promised-based image loading
4307
- * @param {string} src Remote image source, i.e., a URL
4308
- * @param {boolean} isCrossOrigin If `true` allow loading image from a source of another origin.
4309
- * @return {Promise<HTMLImageElement>} Promise resolving to the image once its loaded
4310
- */
4311
- const loadImage = (
4312
- src,
4313
- isCrossOrigin = false,
4314
- timeout = DEFAULT_IMAGE_LOAD_TIMEOUT,
4315
- ) =>
4316
- new Promise((resolve, reject) => {
4317
- const image = new Image();
4318
- if (isCrossOrigin) {
4319
- image.crossOrigin = 'anonymous';
4320
- }
4321
- image.src = src;
4322
- image.onload = () => {
4323
- resolve(image);
4324
- };
4325
- const rejectPromise = () => {
4326
- reject(new Error(IMAGE_LOAD_ERROR));
4327
- };
4328
- image.onerror = rejectPromise;
4329
- setTimeout(rejectPromise, timeout);
4330
- });
4217
+ if (isPixelAligned < 0.5) {
4218
+ gl_Position = modelViewProjection * vec4(state.x, state.y, 0.0, 1.0);
4219
+ } else {
4220
+ vec4 clipSpacePosition = modelViewProjection * vec4(state.x, state.y, 0.0, 1.0);
4221
+ vec2 ndcPosition = clipSpacePosition.xy / clipSpacePosition.w;
4222
+ vec2 pixelPos = 0.5 * (ndcPosition + 1.0) * vec2(drawingBufferWidth, drawingBufferHeight);
4223
+ pixelPos = floor(pixelPos + 0.5); // Snap to nearest pixel
4224
+ vec2 snappedPosition = (pixelPos / vec2(drawingBufferWidth, drawingBufferHeight)) * 2.0 - 1.0;
4225
+ gl_Position = vec4(snappedPosition, 0.0, 1.0);
4226
+ }
4331
4227
 
4332
- /**
4333
- * @deprecated Please use `scatterplot.createTextureFromUrl(url)`
4334
- *
4335
- * Create a Regl texture from an URL.
4336
- * @param {import('regl').Regl} regl Regl instance used for creating the texture.
4337
- * @param {string} url Source URL of the image.
4338
- * @return {Promise<import('regl').Texture2D>} Promise resolving to the texture object.
4339
- */
4340
- const createTextureFromUrl = (
4341
- regl,
4342
- url,
4343
- timeout = DEFAULT_IMAGE_LOAD_TIMEOUT,
4344
- ) =>
4345
- new Promise((resolve, reject) => {
4346
- loadImage(
4347
- url,
4348
- url.indexOf(window.location.origin) !== 0 && url.indexOf('base64') === -1,
4349
- timeout,
4350
- )
4351
- .then((image) => {
4352
- resolve(regl.texture(image));
4353
- })
4354
- .catch((error) => {
4355
- reject(error);
4356
- });
4357
- });
4358
4228
 
4359
- /**
4360
- * Convert a HEX-encoded color to an RGBA-encoded color
4361
- * @param {string} hex HEX-encoded color string.
4362
- * @param {boolean} isNormalize If `true` the returned RGBA values will be
4363
- * normalized to `[0,1]`.
4364
- * @return {array} Triple holding the RGBA values.
4365
- */
4366
- const hexToRgba = (hex, isNormalize = false) => [
4367
- ...hexToRgb(hex, isNormalize),
4368
- 255 ** !isNormalize,
4369
- ];
4229
+ // Determine color index
4230
+ float colorIndexZ = isColoredByZ * floor(state.z * colorMultiplicator);
4231
+ float colorIndexW = isColoredByW * floor(state.w * colorMultiplicator);
4370
4232
 
4371
- const REGEX_IS_HEX = /(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i;
4233
+ // Multiply by the number of color states per color
4234
+ // I.e., normal, active, hover, background, etc.
4235
+ float colorIndex = (colorIndexZ + colorIndexW) * numColorStates;
4372
4236
 
4373
- /**
4374
- * Tests if a string is a valid HEX color encoding
4375
- * @param {string} hex HEX-encoded color string.
4376
- * @return {boolean} If `true` the string is a valid HEX color encoding.
4377
- */
4378
- const isHex = (hex) => REGEX_IS_HEX.test(hex);
4237
+ // Half a "pixel" or "texel" in texture coordinates
4238
+ float colorLinearIndex = colorIndex + globalState;
4239
+
4240
+ // Need to add cEps here to avoid floating point issue that can lead to
4241
+ // dramatic changes in which color is loaded as floor(3/2.9999) = 1 but
4242
+ // floor(3/3.0001) = 0!
4243
+ float colorRowIndex = floor((colorLinearIndex + colorTexEps) / colorTexRes);
4244
+
4245
+ vec2 colorTexIndex = vec2(
4246
+ (colorLinearIndex / colorTexRes) - colorRowIndex + colorTexEps,
4247
+ colorRowIndex / colorTexRes + colorTexEps
4248
+ );
4379
4249
 
4380
- /**
4381
- * Tests if a number is in `[0,1]`.
4382
- * @param {number} x Number to be tested.
4383
- * @return {boolean} If `true` the number is in `[0,1]`.
4384
- */
4385
- const isNormFloat = (x) => x >= 0 && x <= 1;
4250
+ color = texture2D(colorTex, colorTexIndex);
4386
4251
 
4387
- /**
4388
- * Tests if an array consist of normalized numbers that are in `[0,1]` only.
4389
- * @param {array} a Array to be tested
4390
- * @return {boolean} If `true` the array contains only numbers in `[0,1]`.
4391
- */
4392
- const isNormFloatArray = (a) => Array.isArray(a) && a.every(isNormFloat);
4252
+ // Retrieve point size
4253
+ float pointSizeIndexZ = isSizedByZ * floor(state.z * sizeMultiplicator);
4254
+ float pointSizeIndexW = isSizedByW * floor(state.w * sizeMultiplicator);
4255
+ float pointSizeIndex = pointSizeIndexZ + pointSizeIndexW;
4393
4256
 
4394
- /**
4395
- * Computes the cross product to determine the orientation of three points
4396
- * @param {number} x1 X-coordinate of first point
4397
- * @param {number} y1 Y-coordinate of first point
4398
- * @param {number} x2 X-coordinate of second point
4399
- * @param {number} y2 Y-coordinate of second point
4400
- * @param {number} px X-coordinate of test point
4401
- * @param {number} py Y-coordinate of test point
4402
- * @return {number} Positive if counterclockwise, negative if clockwise
4403
- */
4404
- function crossProduct(x1, y1, x2, y2, px, py) {
4405
- return (x2 - x1) * (py - y1) - (px - x1) * (y2 - y1);
4406
- }
4257
+ float pointSizeRowIndex = floor((pointSizeIndex + encodingTexEps) / encodingTexRes);
4258
+ vec2 pointSizeTexIndex = vec2(
4259
+ (pointSizeIndex / encodingTexRes) - pointSizeRowIndex + encodingTexEps,
4260
+ pointSizeRowIndex / encodingTexRes + encodingTexEps
4261
+ );
4262
+ float pointSize = texture2D(encodingTex, pointSizeTexIndex).x;
4407
4263
 
4408
- /**
4409
- * Determines if a point lies within a polygon using the non-zero winding rule.
4410
- * This handles self-intersecting polygons and overlapping areas correctly.
4411
- * @param {Array} polygon 1D list of vertices defining the polygon [x1,y1,x2,y2,...]
4412
- * @param {Array} point Tuple of the form [x,y] to be tested
4413
- * @return {boolean} True if point lies within the polygon
4414
- */
4415
- const isPointInPolygon = (polygon, [px, py] = []) => {
4416
- let winding = 0;
4264
+ // Retrieve opacity
4265
+ ${
4266
+ (() => {
4267
+ // Drawing the inner border of selected points
4268
+ if (globalState === 3) return '';
4417
4269
 
4418
- for (let i = 0, j = polygon.length - 2; i < polygon.length; i += 2) {
4419
- const x1 = polygon[i];
4420
- const y1 = polygon[i + 1];
4421
- const x2 = polygon[j];
4422
- const y2 = polygon[j + 1];
4270
+ // Draw points with opacity encoding or dynamic opacity
4271
+ return `
4272
+ if (isOpacityByDensity < 0.5) {
4273
+ float opacityIndexZ = isOpacityByZ * floor(state.z * opacityMultiplicator);
4274
+ float opacityIndexW = isOpacityByW * floor(state.w * opacityMultiplicator);
4275
+ float opacityIndex = opacityIndexZ + opacityIndexW;
4423
4276
 
4424
- if (y1 <= py) {
4425
- if (y2 > py) {
4426
- const orientation = crossProduct(x1, y1, x2, y2, px, py);
4427
- if (orientation > 0) {
4428
- winding++;
4277
+ float opacityRowIndex = floor((opacityIndex + encodingTexEps) / encodingTexRes);
4278
+ vec2 opacityTexIndex = vec2(
4279
+ (opacityIndex / encodingTexRes) - opacityRowIndex + encodingTexEps,
4280
+ opacityRowIndex / encodingTexRes + encodingTexEps
4281
+ );
4282
+ color.a = texture2D(encodingTex, opacityTexIndex)[${1 + globalState}];
4283
+ } else {
4284
+ color.a = min(1.0, opacityDensity + globalState);
4429
4285
  }
4430
- }
4431
- } else if (y2 <= py) {
4432
- const orientation = crossProduct(x1, y1, x2, y2, px, py);
4433
- if (orientation < 0) {
4434
- winding--;
4435
- }
4436
- }
4437
-
4438
- j = i;
4286
+ `;
4287
+ })()
4439
4288
  }
4440
4289
 
4441
- return winding !== 0;
4442
- };
4290
+ color.a = min(pointOpacityMax, color.a) * pointOpacityScale;
4291
+ finalPointSize = (pointSize * pointScale) + pointSizeExtra;
4292
+ gl_PointSize = finalPointSize;
4293
+ }
4294
+ `;
4443
4295
 
4444
- /**
4445
- * Tests if a variable is a string
4446
- * @param {*} s Variable to be tested
4447
- * @return {boolean} If `true` variable is a string
4448
- */
4449
- const isString = (s) => typeof s === 'string' || s instanceof String;
4296
+ const FRAGMENT_SHADER = `precision highp float;
4450
4297
 
4451
- /**
4452
- * Tests if a number is an interger and in `[0,255]`.
4453
- * @param {number} x Number to be tested.
4454
- * @return {boolean} If `true` the number is an interger and in `[0,255]`.
4455
- */
4456
- const isUint8 = (x) => Number.isInteger(x) && x >= 0 && x <= 255;
4298
+ varying vec4 color;
4457
4299
 
4458
- /**
4459
- * Tests if an array consist of Uint8 numbers only.
4460
- * @param {array} a Array to be tested.
4461
- * @return {boolean} If `true` the array contains only Uint8 numbers.
4462
- */
4463
- const isUint8Array = (a) => Array.isArray(a) && a.every(isUint8);
4300
+ void main() {
4301
+ gl_FragColor = color;
4302
+ }
4303
+ `;
4464
4304
 
4465
- /**
4466
- * Tests if an array is encoding an RGB color.
4467
- * @param {array} rgb Array to be tested
4468
- * @return {boolean} If `true` the array hold a triple of Uint8 numbers or
4469
- * a triple of normalized floats.
4470
- */
4471
- const isRgb = (rgb) =>
4472
- rgb.length === 3 && (isNormFloatArray(rgb) || isUint8Array(rgb));
4305
+ const SHADER$1 = `precision highp float;
4473
4306
 
4474
- /**
4475
- * Tests if an array is encoding an RGBA color.
4476
- * @param {array} rgb Array to be tested
4477
- * @return {boolean} If `true` the array hold a quadruple of Uint8 numbers or
4478
- * a quadruple of normalized floats.
4479
- */
4480
- const isRgba = (rgba) =>
4481
- rgba.length === 4 && (isNormFloatArray(rgba) || isUint8Array(rgba));
4307
+ uniform sampler2D startStateTex;
4308
+ uniform sampler2D endStateTex;
4309
+ uniform float t;
4482
4310
 
4483
- /**
4484
- * Test if a color is multiple colors
4485
- * @param {*} color To be tested
4486
- * @return {boolean} If `true`, `color` is an array of colors.
4487
- */
4488
- const isMultipleColors = (color) =>
4489
- Array.isArray(color) &&
4490
- color.length > 0 &&
4491
- (Array.isArray(color[0]) || isString(color[0]));
4311
+ varying vec2 particleTextureIndex;
4492
4312
 
4493
- /**
4494
- * Test if two arrays contain the same RGBA quadruples
4495
- */
4496
- const isSameRgbas = (a, b) => {
4497
- if (!(Array.isArray(a) && Array.isArray(b)) || a.length !== b.length) {
4498
- return false;
4499
- }
4313
+ void main() {
4314
+ // Interpolate x, y, and value
4315
+ vec3 start = texture2D(startStateTex, particleTextureIndex).xyw;
4316
+ vec3 end = texture2D(endStateTex, particleTextureIndex).xyw;
4317
+ vec3 curr = start * (1.0 - t) + end * t;
4500
4318
 
4501
- if (a.length === 0) {
4502
- return true;
4503
- }
4319
+ // The category cannot be interpolated
4320
+ float endCategory = texture2D(endStateTex, particleTextureIndex).z;
4504
4321
 
4505
- // We need to test whether a and b are arrays of RGBA quadruples
4506
- if (!(Array.isArray(a[0]) && Array.isArray(b[0]))) {
4507
- return false;
4508
- }
4322
+ gl_FragColor = vec4(curr.xy, endCategory, curr.z);
4323
+ }`;
4509
4324
 
4510
- return a.every(([r1, g1, b1, a1], i) => {
4511
- const [r2, g2, b2, a2] = b[i];
4512
- return r1 === r2 && g1 === g2 && b1 === b2 && a1 === a2;
4513
- });
4514
- };
4325
+ const SHADER = `precision highp float;
4515
4326
 
4516
- /**
4517
- * Fast version of `Math.max`. Based on
4518
- * https://jsperf.com/math-min-max-vs-ternary-vs-if/24 `Math.max` is not
4519
- * very fast
4520
- * @param {number} a Value A
4521
- * @param {number} b Value B
4522
- * @return {boolean} If `true` A is greater than B.
4523
- */
4524
- const max = (a, b) => (a > b ? a : b);
4327
+ attribute vec2 position;
4328
+ varying vec2 particleTextureIndex;
4525
4329
 
4526
- /**
4527
- * Fast version of `Math.min`. Based on
4528
- * https://jsperf.com/math-min-max-vs-ternary-vs-if/24 `Math.max` is not
4529
- * very fast
4530
- * @param {number} a Value A
4531
- * @param {number} b Value B
4532
- * @return {boolean} If `true` A is smaller than B.
4533
- */
4534
- const min = (a, b) => (a < b ? a : b);
4330
+ void main() {
4331
+ // map normalized device coords to texture coords
4332
+ particleTextureIndex = 0.5 * (1.0 + position);
4333
+
4334
+ gl_Position = vec4(position, 0, 1);
4335
+ }`;
4535
4336
 
4536
4337
  /**
4537
- * Convert a color to an RGBA color
4538
- * @param {*} color Color to be converted. Currently supports:
4539
- * HEX, RGB, or RGBA.
4540
- * @param {boolean} isNormalize If `true` the returned RGBA values will be
4541
- * normalized to `[0,1]`.
4542
- * @return {array} Quadruple defining an RGBA color.
4338
+ * Check if all GL extensions are supported and enabled and warn otherwise
4339
+ * @param {import('regl').Regl} regl Regl instance to be tested
4340
+ * @param {boolean} silent If `true` the function will not print `console.warn` statements
4341
+ * @return {boolean} If `true` all required GL extensions are supported
4543
4342
  */
4544
- const toRgba = (color, shouldNormalize) => {
4545
- if (isRgba(color)) {
4546
- const isNormalized = isNormFloatArray(color);
4547
- if (
4548
- (shouldNormalize && isNormalized) ||
4549
- !(shouldNormalize || isNormalized)
4550
- ) {
4551
- return color;
4552
- }
4553
- if (shouldNormalize && !isNormalized) {
4554
- return color.map((x) => x / 255);
4555
- }
4556
- return color.map((x) => x * 255);
4557
- }
4558
-
4559
- if (isRgb(color)) {
4560
- const base = 255 ** !shouldNormalize;
4561
- const isNormalized = isNormFloatArray(color);
4562
-
4563
- if (
4564
- (shouldNormalize && isNormalized) ||
4565
- !(shouldNormalize || isNormalized)
4566
- ) {
4567
- return [...color, base];
4568
- }
4569
- if (shouldNormalize && !isNormalized) {
4570
- return [...color.map((x) => x / 255), base];
4571
- }
4572
- return [...color.map((x) => x * 255), base];
4573
- }
4574
-
4575
- if (isHex(color)) {
4576
- return hexToRgba(color, shouldNormalize);
4343
+ const checkReglExtensions = (regl, silent) => {
4344
+ if (!regl) {
4345
+ return false;
4577
4346
  }
4578
-
4579
- // biome-ignore lint/suspicious/noConsole: This is a legitimately useful warning
4580
- console.warn(
4581
- 'Only HEX, RGB, and RGBA are handled by this function. Returning white instead.',
4582
- );
4583
- return shouldNormalize ? [1, 1, 1, 1] : [255, 255, 255, 255];
4347
+ return GL_EXTENSIONS.reduce((every, extension) => {
4348
+ if (!regl.hasExtension(extension)) {
4349
+ if (!silent) {
4350
+ // biome-ignore lint/suspicious/noConsole: This is a legitimately useful warning
4351
+ console.warn(
4352
+ `WebGL: ${extension} extension not supported. Scatterplot might not render properly`,
4353
+ );
4354
+ }
4355
+ return false;
4356
+ }
4357
+ return every;
4358
+ }, true);
4584
4359
  };
4585
4360
 
4586
4361
  /**
4587
- * Flip the key-value pairs of an object
4588
- * @param {object} obj - Object to be flipped
4589
- * @return {object} Flipped object
4362
+ * Create a new Regl instance with `GL_EXTENSIONS` enables
4363
+ * @param {HTMLCanvasElement} canvas Canvas element to be rendered on
4364
+ * @return {import('regl').Regl} New Regl instance
4590
4365
  */
4591
- const flipObj = (obj) =>
4592
- Object.entries(obj).reduce((out, [key, value]) => {
4593
- if (out[value]) {
4594
- out[value] = [...out[value], key];
4366
+ const createRegl = (canvas) => {
4367
+ const gl = canvas.getContext('webgl', {
4368
+ antialias: true,
4369
+ preserveDrawingBuffer: true,
4370
+ });
4371
+ const extensions = [];
4372
+
4373
+ // Needed to run the tests properly as the headless-gl doesn't support all
4374
+ // extensions, which is fine for the functional tests.
4375
+ for (const extension of GL_EXTENSIONS) {
4376
+ if (gl.getExtension(extension)) {
4377
+ extensions.push(extension);
4595
4378
  } else {
4596
- out[value] = key;
4379
+ // biome-ignore lint/suspicious/noConsole: This is a legitimately useful warning
4380
+ console.warn(
4381
+ `WebGL: ${extension} extension not supported. Scatterplot might not render properly`,
4382
+ );
4597
4383
  }
4598
- return out;
4599
- }, {});
4384
+ }
4600
4385
 
4601
- const rgbBrightness = (rgb) =>
4602
- 0.21 * rgb[0] + 0.72 * rgb[1] + 0.07 * rgb[2];
4386
+ return createOriginalRegl({ gl, extensions });
4387
+ };
4603
4388
 
4604
4389
  /**
4605
- * Clip a number between min and max
4606
- * @param {number} value - The value to be clipped
4607
- * @param {number} minValue - The minimum value
4608
- * @param {number} maxValue - The maximum value
4609
- * @return {number} The clipped value
4390
+ * L2 distance between a pair of 2D points
4391
+ * @param {number} x1 X coordinate of the first point
4392
+ * @param {number} y1 Y coordinate of the first point
4393
+ * @param {number} x2 X coordinate of the second point
4394
+ * @param {number} y2 Y coordinate of the first point
4395
+ * @return {number} L2 distance
4610
4396
  */
4611
- const clip = (value, minValue, maxValue) =>
4612
- Math.min(maxValue, Math.max(minValue, value));
4397
+ const dist = (x1, y1, x2, y2) =>
4398
+ Math.sqrt((x1 - x2) ** 2 + (y1 - y2) ** 2);
4613
4399
 
4614
4400
  /**
4615
- * Convert object- or array-oriented points to array-oriented points
4616
- * @param {import('./types').Points} points - The point data
4617
- * @return {number[][]} Array-oriented points
4401
+ * Get the bounding box of a set of 2D positions
4402
+ * @param {array} positions2d 2D positions to be checked
4403
+ * @return {array} Quadruple of form `[xMin, yMin, xMax, yMax]` defining the
4404
+ * bounding box
4618
4405
  */
4619
- const toArrayOrientedPoints = (points) =>
4620
- new Promise((resolve, reject) => {
4621
- if (!points || Array.isArray(points)) {
4622
- resolve(points);
4623
- } else {
4624
- const length =
4625
- Array.isArray(points.x) || ArrayBuffer.isView(points.x)
4626
- ? points.x.length
4627
- : 0;
4406
+ const getBBox = (positions2d) => {
4407
+ let xMin = Number.POSITIVE_INFINITY;
4408
+ let xMax = Number.NEGATIVE_INFINITY;
4409
+ let yMin = Number.POSITIVE_INFINITY;
4410
+ let yMax = Number.NEGATIVE_INFINITY;
4628
4411
 
4629
- const getX =
4630
- (Array.isArray(points.x) || ArrayBuffer.isView(points.x)) &&
4631
- ((i) => points.x[i]);
4632
- const getY =
4633
- (Array.isArray(points.y) || ArrayBuffer.isView(points.y)) &&
4634
- ((i) => points.y[i]);
4635
- const getL =
4636
- (Array.isArray(points.line) || ArrayBuffer.isView(points.line)) &&
4637
- ((i) => points.line[i]);
4412
+ for (let i = 0; i < positions2d.length; i += 2) {
4413
+ xMin = positions2d[i] < xMin ? positions2d[i] : xMin;
4414
+ xMax = positions2d[i] > xMax ? positions2d[i] : xMax;
4415
+ yMin = positions2d[i + 1] < yMin ? positions2d[i + 1] : yMin;
4416
+ yMax = positions2d[i + 1] > yMax ? positions2d[i + 1] : yMax;
4417
+ }
4638
4418
 
4639
- // biome-ignore lint/style/useNamingConvention: LO stands for line and order
4640
- const getLO =
4641
- (Array.isArray(points.lineOrder) ||
4642
- ArrayBuffer.isView(points.lineOrder)) &&
4643
- ((i) => points.lineOrder[i]);
4419
+ return [xMin, yMin, xMax, yMax];
4420
+ };
4644
4421
 
4645
- const components = Object.keys(points);
4646
- const getZ = (() => {
4647
- const z = components.find((c) => Z_NAMES.has(c));
4648
- return (
4649
- z &&
4650
- (Array.isArray(points[z]) || ArrayBuffer.isView(points[z])) &&
4651
- ((i) => points[z][i])
4652
- );
4653
- })();
4654
- const getW = (() => {
4655
- const w = components.find((c) => W_NAMES.has(c));
4656
- return (
4657
- w &&
4658
- (Array.isArray(points[w]) || ArrayBuffer.isView(points[w])) &&
4659
- ((i) => points[w][i])
4660
- );
4661
- })();
4422
+ /**
4423
+ * Test whether a bounding box is actually specifying an area
4424
+ * @param {array} bBox The bounding box to be checked
4425
+ * @return {array} `true` if the bounding box is valid
4426
+ */
4427
+ const isValidBBox = ([xMin, yMin, xMax, yMax]) =>
4428
+ Number.isFinite(xMin) &&
4429
+ Number.isFinite(yMin) &&
4430
+ Number.isFinite(xMax) &&
4431
+ Number.isFinite(yMax) &&
4432
+ xMax - xMin > 0 &&
4433
+ yMax - yMin > 0;
4662
4434
 
4663
- if (getX && getY && getZ && getW && getL && getLO) {
4664
- resolve(
4665
- points.x.map((x, i) => [
4666
- x,
4667
- getY(i),
4668
- getZ(i),
4669
- getW(i),
4670
- getL(i),
4671
- getLO(i),
4672
- ]),
4673
- );
4674
- } else if (getX && getY && getZ && getW && getL) {
4675
- resolve(
4676
- Array.from({ length }, (_, i) => [
4677
- getX(i),
4678
- getY(i),
4679
- getZ(i),
4680
- getW(i),
4681
- getL(i),
4682
- ]),
4683
- );
4684
- } else if (getX && getY && getZ && getW) {
4685
- resolve(
4686
- Array.from({ length }, (_, i) => [
4687
- getX(i),
4688
- getY(i),
4689
- getZ(i),
4690
- getW(i),
4691
- ]),
4692
- );
4693
- } else if (getX && getY && getZ) {
4694
- resolve(Array.from({ length }, (_, i) => [getX(i), getY(i), getZ(i)]));
4695
- } else if (getX && getY) {
4696
- resolve(Array.from({ length }, (_, i) => [getX(i), getY(i)]));
4697
- } else {
4698
- reject(new Error('You need to specify at least x and y'));
4699
- }
4700
- }
4701
- });
4435
+ const REGEX_HEX_TO_RGB = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
4702
4436
 
4703
- const isHorizontalLine = (annotation) =>
4704
- Number.isFinite(annotation.y) && !('x' in annotation);
4437
+ /**
4438
+ * Convert a HEX-encoded color to an RGB-encoded color
4439
+ * @param {string} hex HEX-encoded color string.
4440
+ * @param {boolean} isNormalize If `true` the returned RGB values will be
4441
+ * normalized to `[0,1]`.
4442
+ * @return {array} Triple holding the RGB values.
4443
+ */
4444
+ const hexToRgb = (hex, isNormalize = false) =>
4445
+ hex
4446
+ .replace(REGEX_HEX_TO_RGB, (_m, r, g, b) => `#${r}${r}${g}${g}${b}${b}`)
4447
+ .substring(1)
4448
+ .match(/.{2}/g)
4449
+ .map((x) => Number.parseInt(x, 16) / 255 ** isNormalize);
4705
4450
 
4706
- const isVerticalLine = (annotation) =>
4707
- Number.isFinite(annotation.x) && !('y' in annotation);
4451
+ const isConditionalArray = (a, condition, { minLength = 0 } = {}) =>
4452
+ Array.isArray(a) && a.length >= minLength && a.every(condition);
4708
4453
 
4709
- const isDomRect = (annotation) =>
4710
- Number.isFinite(annotation.x) &&
4711
- Number.isFinite(annotation.y) &&
4712
- Number.isFinite(annotation.width) &&
4713
- Number.isFinite(annotation.height);
4454
+ const isPositiveNumber = (x) => !Number.isNaN(+x) && +x >= 0;
4714
4455
 
4715
- const isRect = (annotation) =>
4716
- Number.isFinite(annotation.x1) &&
4717
- Number.isFinite(annotation.y1) &&
4718
- Number.isFinite(annotation.x2) &&
4719
- Number.isFinite(annotation.x2);
4456
+ const isStrictlyPositiveNumber = (x) => !Number.isNaN(+x) && +x > 0;
4720
4457
 
4721
- const isPolygon = (annotation) =>
4722
- 'vertices' in annotation && annotation.vertices.length > 1;
4458
+ /**
4459
+ * Create a function to limit choices to a predefined list
4460
+ * @param {array} choices Array of acceptable choices
4461
+ * @param {*} defaultOption Default choice
4462
+ * @return {function} Function limiting the choices
4463
+ */
4464
+ const limit = (choices, defaultChoice) => (choice) =>
4465
+ choices.indexOf(choice) >= 0 ? choice : defaultChoice;
4723
4466
 
4724
- const insertionSort = (array) => {
4725
- const end = array.length;
4726
- for (let i = 1; i < end; i++) {
4727
- // Choosing the first element in our unsorted subarray
4728
- const current = array[i];
4729
- // The last element of our sorted subarray
4730
- let j = i - 1;
4731
- while (j > -1 && current < array[j]) {
4732
- array[j + 1] = array[j];
4733
- j--;
4467
+ /**
4468
+ * Promised-based image loading
4469
+ * @param {string} src Remote image source, i.e., a URL
4470
+ * @param {boolean} isCrossOrigin If `true` allow loading image from a source of another origin.
4471
+ * @return {Promise<HTMLImageElement>} Promise resolving to the image once its loaded
4472
+ */
4473
+ const loadImage = (
4474
+ src,
4475
+ isCrossOrigin = false,
4476
+ timeout = DEFAULT_IMAGE_LOAD_TIMEOUT,
4477
+ ) =>
4478
+ new Promise((resolve, reject) => {
4479
+ const image = new Image();
4480
+ if (isCrossOrigin) {
4481
+ image.crossOrigin = 'anonymous';
4734
4482
  }
4735
- array[j + 1] = current;
4736
- }
4737
- return array;
4738
- };
4739
-
4740
- const createRenderer = (
4741
- /** @type {Partial<import('./types').RendererOptions>} */ options = {},
4742
- ) => {
4743
- let {
4744
- regl,
4745
- canvas = document.createElement('canvas'),
4746
- gamma = DEFAULT_GAMMA,
4747
- } = options;
4483
+ image.src = src;
4484
+ image.onload = () => {
4485
+ resolve(image);
4486
+ };
4487
+ const rejectPromise = () => {
4488
+ reject(new Error(IMAGE_LOAD_ERROR));
4489
+ };
4490
+ image.onerror = rejectPromise;
4491
+ setTimeout(rejectPromise, timeout);
4492
+ });
4748
4493
 
4749
- let isDestroyed = false;
4494
+ /**
4495
+ * @deprecated Please use `scatterplot.createTextureFromUrl(url)`
4496
+ *
4497
+ * Create a Regl texture from an URL.
4498
+ * @param {import('regl').Regl} regl Regl instance used for creating the texture.
4499
+ * @param {string} url Source URL of the image.
4500
+ * @return {Promise<import('regl').Texture2D>} Promise resolving to the texture object.
4501
+ */
4502
+ const createTextureFromUrl = (
4503
+ regl,
4504
+ url,
4505
+ timeout = DEFAULT_IMAGE_LOAD_TIMEOUT,
4506
+ ) =>
4507
+ new Promise((resolve, reject) => {
4508
+ loadImage(
4509
+ url,
4510
+ url.indexOf(window.location.origin) !== 0 && url.indexOf('base64') === -1,
4511
+ timeout,
4512
+ )
4513
+ .then((image) => {
4514
+ resolve(regl.texture(image));
4515
+ })
4516
+ .catch((error) => {
4517
+ reject(error);
4518
+ });
4519
+ });
4750
4520
 
4751
- if (!regl) {
4752
- regl = createRegl(canvas);
4753
- }
4521
+ /**
4522
+ * Convert a HEX-encoded color to an RGBA-encoded color
4523
+ * @param {string} hex HEX-encoded color string.
4524
+ * @param {boolean} isNormalize If `true` the returned RGBA values will be
4525
+ * normalized to `[0,1]`.
4526
+ * @return {array} Triple holding the RGBA values.
4527
+ */
4528
+ const hexToRgba = (hex, isNormalize = false) => [
4529
+ ...hexToRgb(hex, isNormalize),
4530
+ 255 ** !isNormalize,
4531
+ ];
4754
4532
 
4755
- const isSupportingAllGlExtensions = checkReglExtensions(regl);
4533
+ const REGEX_IS_HEX = /(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i;
4756
4534
 
4757
- const fboRes = [canvas.width, canvas.height];
4758
- const fbo = regl.framebuffer({
4759
- width: fboRes[0],
4760
- height: fboRes[1],
4761
- colorFormat: 'rgba',
4762
- colorType: 'float',
4763
- });
4535
+ /**
4536
+ * Tests if a string is a valid HEX color encoding
4537
+ * @param {string} hex HEX-encoded color string.
4538
+ * @return {boolean} If `true` the string is a valid HEX color encoding.
4539
+ */
4540
+ const isHex = (hex) => REGEX_IS_HEX.test(hex);
4764
4541
 
4765
- /**
4766
- * Render the float32 framebuffer to the internal canvas
4767
- *
4768
- * From https://observablehq.com/@rreusser/selecting-the-right-opacity-for-2d-point-clouds
4769
- */
4770
- const renderToCanvas = regl({
4771
- vert: `
4772
- precision highp float;
4773
- attribute vec2 xy;
4774
- void main () {
4775
- gl_Position = vec4(xy, 0, 1);
4776
- }`,
4777
- frag: `
4778
- precision highp float;
4779
- uniform vec2 srcRes;
4780
- uniform sampler2D src;
4781
- uniform float gamma;
4542
+ /**
4543
+ * Tests if a number is in `[0,1]`.
4544
+ * @param {number} x Number to be tested.
4545
+ * @return {boolean} If `true` the number is in `[0,1]`.
4546
+ */
4547
+ const isNormFloat = (x) => x >= 0 && x <= 1;
4782
4548
 
4783
- vec3 approxLinearToSRGB (vec3 rgb, float gamma) {
4784
- return pow(clamp(rgb, vec3(0), vec3(1)), vec3(1.0 / gamma));
4785
- }
4549
+ /**
4550
+ * Tests if an array consist of normalized numbers that are in `[0,1]` only.
4551
+ * @param {array} a Array to be tested
4552
+ * @return {boolean} If `true` the array contains only numbers in `[0,1]`.
4553
+ */
4554
+ const isNormFloatArray = (a) => Array.isArray(a) && a.every(isNormFloat);
4786
4555
 
4787
- void main () {
4788
- vec4 color = texture2D(src, gl_FragCoord.xy / srcRes);
4789
- gl_FragColor = vec4(approxLinearToSRGB(color.rgb, gamma), color.a);
4790
- }`,
4791
- attributes: {
4792
- xy: [-4, -4, 4, -4, 0, 4],
4793
- },
4794
- uniforms: {
4795
- src: () => fbo,
4796
- srcRes: () => fboRes,
4797
- gamma: () => gamma,
4798
- },
4799
- count: 3,
4800
- depth: { enable: false },
4801
- blend: {
4802
- enable: true,
4803
- func: {
4804
- // biome-ignore lint/style/useNamingConvention: Regl internal
4805
- srcRGB: 'one',
4806
- srcAlpha: 'one',
4807
- // biome-ignore lint/style/useNamingConvention: Regl internal
4808
- dstRGB: 'one minus src alpha',
4809
- dstAlpha: 'one minus src alpha',
4810
- },
4811
- },
4812
- });
4556
+ /**
4557
+ * Computes the cross product to determine the orientation of three points
4558
+ * @param {number} x1 X-coordinate of first point
4559
+ * @param {number} y1 Y-coordinate of first point
4560
+ * @param {number} x2 X-coordinate of second point
4561
+ * @param {number} y2 Y-coordinate of second point
4562
+ * @param {number} px X-coordinate of test point
4563
+ * @param {number} py Y-coordinate of test point
4564
+ * @return {number} Positive if counterclockwise, negative if clockwise
4565
+ */
4566
+ function crossProduct(x1, y1, x2, y2, px, py) {
4567
+ return (x2 - x1) * (py - y1) - (px - x1) * (y2 - y1);
4568
+ }
4813
4569
 
4814
- /**
4815
- * Copy the pixels from the internal canvas onto the target canvas
4816
- */
4817
- const copyTo = (targetCanvas) => {
4818
- const ctx = targetCanvas.getContext('2d');
4819
- ctx.clearRect(0, 0, targetCanvas.width, targetCanvas.height);
4820
- ctx.drawImage(
4821
- canvas,
4822
- (canvas.width - targetCanvas.width) / 2,
4823
- (canvas.height - targetCanvas.height) / 2,
4824
- targetCanvas.width,
4825
- targetCanvas.height,
4826
- 0,
4827
- 0,
4828
- targetCanvas.width,
4829
- targetCanvas.height,
4830
- );
4831
- };
4570
+ /**
4571
+ * Determines if a point lies within a polygon using the non-zero winding rule.
4572
+ * This handles self-intersecting polygons and overlapping areas correctly.
4573
+ * @param {Array} polygon 1D list of vertices defining the polygon [x1,y1,x2,y2,...]
4574
+ * @param {Array} point Tuple of the form [x,y] to be tested
4575
+ * @return {boolean} True if point lies within the polygon
4576
+ */
4577
+ const isPointInPolygon = (polygon, [px, py] = []) => {
4578
+ let winding = 0;
4832
4579
 
4833
- /**
4834
- * The render function
4835
- */
4836
- const render = (
4837
- /** @type {(): void} */ draw,
4838
- /** @type {HTMLCanvasElement} */ targetCanvas,
4839
- ) => {
4840
- // Clear internal canvas
4841
- regl.clear(CLEAR_OPTIONS);
4842
- fbo.use(() => {
4843
- // Clear framebuffer
4844
- regl.clear(CLEAR_OPTIONS);
4845
- draw();
4846
- });
4847
- renderToCanvas();
4848
- copyTo(targetCanvas);
4849
- };
4580
+ for (let i = 0, j = polygon.length - 2; i < polygon.length; i += 2) {
4581
+ const x1 = polygon[i];
4582
+ const y1 = polygon[i + 1];
4583
+ const x2 = polygon[j];
4584
+ const y2 = polygon[j + 1];
4850
4585
 
4851
- /**
4852
- * Update Regl's viewport, drawingBufferWidth, and drawingBufferHeight
4853
- *
4854
- * @description Call this method after the viewport has changed, e.g., width
4855
- * or height have been altered
4856
- */
4857
- const refresh = () => {
4858
- regl.poll();
4859
- };
4586
+ if (y1 <= py) {
4587
+ if (y2 > py) {
4588
+ const orientation = crossProduct(x1, y1, x2, y2, px, py);
4589
+ if (orientation > 0) {
4590
+ winding++;
4591
+ }
4592
+ }
4593
+ } else if (y2 <= py) {
4594
+ const orientation = crossProduct(x1, y1, x2, y2, px, py);
4595
+ if (orientation < 0) {
4596
+ winding--;
4597
+ }
4598
+ }
4860
4599
 
4861
- const drawFns = new Set();
4600
+ j = i;
4601
+ }
4862
4602
 
4863
- /**
4864
- * Register an draw function that is going to be invoked on every animation
4865
- * frame.
4866
- */
4867
- const onFrame = (/** @type {(): void} */ draw) => {
4868
- drawFns.add(draw);
4869
- return () => {
4870
- drawFns.delete(draw);
4871
- };
4872
- };
4603
+ return winding !== 0;
4604
+ };
4873
4605
 
4874
- const frame = regl.frame(() => {
4875
- const iterator = drawFns.values();
4876
- let result = iterator.next();
4877
- while (!result.done) {
4878
- result.value(); // The draw function
4879
- result = iterator.next();
4880
- }
4881
- });
4606
+ /**
4607
+ * Tests if a variable is a string
4608
+ * @param {*} s Variable to be tested
4609
+ * @return {boolean} If `true` variable is a string
4610
+ */
4611
+ const isString = (s) => typeof s === 'string' || s instanceof String;
4882
4612
 
4883
- const resize = (
4884
- /** @type {number} */ customWidth,
4885
- /** @type {number} */ customHeight,
4886
- ) => {
4887
- // We need to limit the width and height by the screen size to prevent
4888
- // a bug in VSCode where the window height is said to be taller than the
4889
- // screen height. The problem with too large dimensions is that at some
4890
- // point WebGL will break down because there's an upper limit on how large
4891
- // any buffer and texture can be. It also harms the performance quite a bit.
4892
- //
4893
- // By restricting the widht/height to the screen size we should have a safe
4894
- // upper limit for the canvas size.
4895
- //
4896
- // @see
4897
- // https://github.com/microsoft/vscode/issues/225808
4898
- // https://github.com/flekschas/jupyter-scatter/issues/37
4899
- const width =
4900
- customWidth === undefined
4901
- ? Math.min(window.innerWidth, window.screen.availWidth)
4902
- : customWidth;
4903
- const height =
4904
- customHeight === undefined
4905
- ? Math.min(window.innerHeight, window.screen.availHeight)
4906
- : customHeight;
4907
- canvas.width = width * window.devicePixelRatio;
4908
- canvas.height = height * window.devicePixelRatio;
4909
- fboRes[0] = canvas.width;
4910
- fboRes[1] = canvas.height;
4911
- fbo.resize(...fboRes);
4912
- };
4613
+ /**
4614
+ * Tests if a number is an interger and in `[0,255]`.
4615
+ * @param {number} x Number to be tested.
4616
+ * @return {boolean} If `true` the number is an interger and in `[0,255]`.
4617
+ */
4618
+ const isUint8 = (x) => Number.isInteger(x) && x >= 0 && x <= 255;
4913
4619
 
4914
- const resizeHandler = () => {
4915
- resize();
4916
- };
4620
+ /**
4621
+ * Tests if an array consist of Uint8 numbers only.
4622
+ * @param {array} a Array to be tested.
4623
+ * @return {boolean} If `true` the array contains only Uint8 numbers.
4624
+ */
4625
+ const isUint8Array = (a) => Array.isArray(a) && a.every(isUint8);
4917
4626
 
4918
- if (!options.canvas) {
4919
- window.addEventListener('resize', resizeHandler);
4920
- window.addEventListener('orientationchange', resizeHandler);
4921
- resize();
4922
- }
4627
+ /**
4628
+ * Tests if an array is encoding an RGB color.
4629
+ * @param {array} rgb Array to be tested
4630
+ * @return {boolean} If `true` the array hold a triple of Uint8 numbers or
4631
+ * a triple of normalized floats.
4632
+ */
4633
+ const isRgb = (rgb) =>
4634
+ rgb.length === 3 && (isNormFloatArray(rgb) || isUint8Array(rgb));
4923
4635
 
4924
- /**
4925
- * Destroy the renderer to free resources and cancel animation frames
4926
- */
4927
- const destroy = () => {
4928
- isDestroyed = true;
4929
- window.removeEventListener('resize', resizeHandler);
4930
- window.removeEventListener('orientationchange', resizeHandler);
4931
- frame.cancel();
4932
- canvas = undefined;
4933
- regl.destroy();
4934
- regl = undefined;
4935
- };
4636
+ /**
4637
+ * Tests if an array is encoding an RGBA color.
4638
+ * @param {array} rgb Array to be tested
4639
+ * @return {boolean} If `true` the array hold a quadruple of Uint8 numbers or
4640
+ * a quadruple of normalized floats.
4641
+ */
4642
+ const isRgba = (rgba) =>
4643
+ rgba.length === 4 && (isNormFloatArray(rgba) || isUint8Array(rgba));
4936
4644
 
4937
- return {
4938
- /**
4939
- * Get the associated canvas element
4940
- * @return {HTMLCanvasElement} The associated canvas element
4941
- */
4942
- get canvas() {
4943
- return canvas;
4944
- },
4945
- /**
4946
- * Get the associated Regl instance
4947
- * @return {import('regl').Regl} The associated Regl instance
4948
- */
4949
- get regl() {
4950
- return regl;
4951
- },
4952
- /**
4953
- * Get the gamma value
4954
- * @return {number} The gamma value
4955
- */
4956
- get gamma() {
4957
- return gamma;
4958
- },
4959
- /**
4960
- * Set gamma to a new value
4961
- * @param {number} newGamma - The new gamma value
4962
- */
4963
- set gamma(newGamma) {
4964
- gamma = +newGamma;
4965
- },
4966
- /**
4967
- * Get whether the browser supports all necessary WebGL features
4968
- * @return {boolean} If `true` the browser supports all necessary WebGL features
4969
- */
4970
- get isSupported() {
4971
- return isSupportingAllGlExtensions;
4972
- },
4973
- /**
4974
- * Get whether the renderer (and its Regl instance) is destroyed
4975
- * @return {boolean} If `true` the renderer is destroyed
4976
- */
4977
- get isDestroyed() {
4978
- return isDestroyed;
4979
- },
4980
- render,
4981
- resize,
4982
- onFrame,
4983
- refresh,
4984
- destroy,
4985
- };
4986
- };
4645
+ /**
4646
+ * Test if a color is multiple colors
4647
+ * @param {*} color To be tested
4648
+ * @return {boolean} If `true`, `color` is an array of colors.
4649
+ */
4650
+ const isMultipleColors = (color) =>
4651
+ Array.isArray(color) &&
4652
+ color.length > 0 &&
4653
+ (Array.isArray(color[0]) || isString(color[0]));
4987
4654
 
4988
- const FRAGMENT_SHADER$2 = `
4989
- precision mediump float;
4655
+ /**
4656
+ * Test if two arrays contain the same RGBA quadruples
4657
+ */
4658
+ const isSameRgbas = (a, b) => {
4659
+ if (!(Array.isArray(a) && Array.isArray(b)) || a.length !== b.length) {
4660
+ return false;
4661
+ }
4990
4662
 
4991
- uniform sampler2D texture;
4663
+ if (a.length === 0) {
4664
+ return true;
4665
+ }
4992
4666
 
4993
- varying vec2 uv;
4667
+ // We need to test whether a and b are arrays of RGBA quadruples
4668
+ if (!(Array.isArray(a[0]) && Array.isArray(b[0]))) {
4669
+ return false;
4670
+ }
4994
4671
 
4995
- void main () {
4996
- gl_FragColor = texture2D(texture, uv);
4997
- }
4998
- `;
4672
+ return a.every(([r1, g1, b1, a1], i) => {
4673
+ const [r2, g2, b2, a2] = b[i];
4674
+ return r1 === r2 && g1 === g2 && b1 === b2 && a1 === a2;
4675
+ });
4676
+ };
4999
4677
 
5000
- const VERTEX_SHADER = `
5001
- precision mediump float;
4678
+ /**
4679
+ * Fast version of `Math.max`. Based on
4680
+ * https://jsperf.com/math-min-max-vs-ternary-vs-if/24 `Math.max` is not
4681
+ * very fast
4682
+ * @param {number} a Value A
4683
+ * @param {number} b Value B
4684
+ * @return {boolean} If `true` A is greater than B.
4685
+ */
4686
+ const max = (a, b) => (a > b ? a : b);
5002
4687
 
5003
- uniform mat4 modelViewProjection;
4688
+ /**
4689
+ * Fast version of `Math.min`. Based on
4690
+ * https://jsperf.com/math-min-max-vs-ternary-vs-if/24 `Math.max` is not
4691
+ * very fast
4692
+ * @param {number} a Value A
4693
+ * @param {number} b Value B
4694
+ * @return {boolean} If `true` A is smaller than B.
4695
+ */
4696
+ const min = (a, b) => (a < b ? a : b);
5004
4697
 
5005
- attribute vec2 position;
4698
+ /**
4699
+ * Convert a color to an RGBA color
4700
+ * @param {*} color Color to be converted. Currently supports:
4701
+ * HEX, RGB, or RGBA.
4702
+ * @param {boolean} isNormalize If `true` the returned RGBA values will be
4703
+ * normalized to `[0,1]`.
4704
+ * @return {array} Quadruple defining an RGBA color.
4705
+ */
4706
+ const toRgba = (color, shouldNormalize) => {
4707
+ if (isRgba(color)) {
4708
+ const isNormalized = isNormFloatArray(color);
4709
+ if (
4710
+ (shouldNormalize && isNormalized) ||
4711
+ !(shouldNormalize || isNormalized)
4712
+ ) {
4713
+ return color;
4714
+ }
4715
+ if (shouldNormalize && !isNormalized) {
4716
+ return color.map((x) => x / 255);
4717
+ }
4718
+ return color.map((x) => x * 255);
4719
+ }
5006
4720
 
5007
- varying vec2 uv;
4721
+ if (isRgb(color)) {
4722
+ const base = 255 ** !shouldNormalize;
4723
+ const isNormalized = isNormFloatArray(color);
5008
4724
 
5009
- void main () {
5010
- uv = position;
5011
- gl_Position = modelViewProjection * vec4(-1.0 + 2.0 * uv.x, 1.0 - 2.0 * uv.y, 0, 1);
5012
- }
5013
- `;
4725
+ if (
4726
+ (shouldNormalize && isNormalized) ||
4727
+ !(shouldNormalize || isNormalized)
4728
+ ) {
4729
+ return [...color, base];
4730
+ }
4731
+ if (shouldNormalize && !isNormalized) {
4732
+ return [...color.map((x) => x / 255), base];
4733
+ }
4734
+ return [...color.map((x) => x * 255), base];
4735
+ }
5014
4736
 
5015
- const FRAGMENT_SHADER$1 = `precision highp float;
4737
+ if (isHex(color)) {
4738
+ return hexToRgba(color, shouldNormalize);
4739
+ }
5016
4740
 
5017
- varying vec4 color;
4741
+ // biome-ignore lint/suspicious/noConsole: This is a legitimately useful warning
4742
+ console.warn(
4743
+ 'Only HEX, RGB, and RGBA are handled by this function. Returning white instead.',
4744
+ );
4745
+ return shouldNormalize ? [1, 1, 1, 1] : [255, 255, 255, 255];
4746
+ };
5018
4747
 
5019
- void main() {
5020
- gl_FragColor = color;
5021
- }
5022
- `;
4748
+ /**
4749
+ * Flip the key-value pairs of an object
4750
+ * @param {object} obj - Object to be flipped
4751
+ * @return {object} Flipped object
4752
+ */
4753
+ const flipObj = (obj) =>
4754
+ Object.entries(obj).reduce((out, [key, value]) => {
4755
+ if (out[value]) {
4756
+ out[value] = [...out[value], key];
4757
+ } else {
4758
+ out[value] = key;
4759
+ }
4760
+ return out;
4761
+ }, {});
5023
4762
 
5024
- const SHADER$1 = `precision highp float;
4763
+ const rgbBrightness = (rgb) =>
4764
+ 0.21 * rgb[0] + 0.72 * rgb[1] + 0.07 * rgb[2];
5025
4765
 
5026
- uniform sampler2D startStateTex;
5027
- uniform sampler2D endStateTex;
5028
- uniform float t;
4766
+ /**
4767
+ * Clip a number between min and max
4768
+ * @param {number} value - The value to be clipped
4769
+ * @param {number} minValue - The minimum value
4770
+ * @param {number} maxValue - The maximum value
4771
+ * @return {number} The clipped value
4772
+ */
4773
+ const clip = (value, minValue, maxValue) =>
4774
+ Math.min(maxValue, Math.max(minValue, value));
5029
4775
 
5030
- varying vec2 particleTextureIndex;
4776
+ /**
4777
+ * Convert object- or array-oriented points to array-oriented points
4778
+ * @param {import('./types').Points} points - The point data
4779
+ * @return {number[][]} Array-oriented points
4780
+ */
4781
+ const toArrayOrientedPoints = (points) =>
4782
+ new Promise((resolve, reject) => {
4783
+ if (!points || Array.isArray(points)) {
4784
+ resolve(points);
4785
+ } else {
4786
+ const length =
4787
+ Array.isArray(points.x) || ArrayBuffer.isView(points.x)
4788
+ ? points.x.length
4789
+ : 0;
5031
4790
 
5032
- void main() {
5033
- // Interpolate x, y, and value
5034
- vec3 start = texture2D(startStateTex, particleTextureIndex).xyw;
5035
- vec3 end = texture2D(endStateTex, particleTextureIndex).xyw;
5036
- vec3 curr = start * (1.0 - t) + end * t;
4791
+ const getX =
4792
+ (Array.isArray(points.x) || ArrayBuffer.isView(points.x)) &&
4793
+ ((i) => points.x[i]);
4794
+ const getY =
4795
+ (Array.isArray(points.y) || ArrayBuffer.isView(points.y)) &&
4796
+ ((i) => points.y[i]);
4797
+ const getL =
4798
+ (Array.isArray(points.line) || ArrayBuffer.isView(points.line)) &&
4799
+ ((i) => points.line[i]);
5037
4800
 
5038
- // The category cannot be interpolated
5039
- float endCategory = texture2D(endStateTex, particleTextureIndex).z;
4801
+ const getLO =
4802
+ (Array.isArray(points.lineOrder) ||
4803
+ ArrayBuffer.isView(points.lineOrder)) &&
4804
+ ((i) => points.lineOrder[i]);
5040
4805
 
5041
- gl_FragColor = vec4(curr.xy, endCategory, curr.z);
5042
- }`;
4806
+ const components = Object.keys(points);
4807
+ const getZ = (() => {
4808
+ const z = components.find((c) => Z_NAMES.has(c));
4809
+ return (
4810
+ z &&
4811
+ (Array.isArray(points[z]) || ArrayBuffer.isView(points[z])) &&
4812
+ ((i) => points[z][i])
4813
+ );
4814
+ })();
4815
+ const getW = (() => {
4816
+ const w = components.find((c) => W_NAMES.has(c));
4817
+ return (
4818
+ w &&
4819
+ (Array.isArray(points[w]) || ArrayBuffer.isView(points[w])) &&
4820
+ ((i) => points[w][i])
4821
+ );
4822
+ })();
5043
4823
 
5044
- const SHADER = `precision highp float;
4824
+ if (getX && getY && getZ && getW && getL && getLO) {
4825
+ resolve(
4826
+ points.x.map((x, i) => [
4827
+ x,
4828
+ getY(i),
4829
+ getZ(i),
4830
+ getW(i),
4831
+ getL(i),
4832
+ getLO(i),
4833
+ ]),
4834
+ );
4835
+ } else if (getX && getY && getZ && getW && getL) {
4836
+ resolve(
4837
+ Array.from({ length }, (_, i) => [
4838
+ getX(i),
4839
+ getY(i),
4840
+ getZ(i),
4841
+ getW(i),
4842
+ getL(i),
4843
+ ]),
4844
+ );
4845
+ } else if (getX && getY && getZ && getW) {
4846
+ resolve(
4847
+ Array.from({ length }, (_, i) => [
4848
+ getX(i),
4849
+ getY(i),
4850
+ getZ(i),
4851
+ getW(i),
4852
+ ]),
4853
+ );
4854
+ } else if (getX && getY && getZ) {
4855
+ resolve(Array.from({ length }, (_, i) => [getX(i), getY(i), getZ(i)]));
4856
+ } else if (getX && getY) {
4857
+ resolve(Array.from({ length }, (_, i) => [getX(i), getY(i)]));
4858
+ } else {
4859
+ reject(new Error('You need to specify at least x and y'));
4860
+ }
4861
+ }
4862
+ });
5045
4863
 
5046
- attribute vec2 position;
5047
- varying vec2 particleTextureIndex;
4864
+ const isHorizontalLine = (annotation) =>
4865
+ Number.isFinite(annotation.y) && !('x' in annotation);
5048
4866
 
5049
- void main() {
5050
- // map normalized device coords to texture coords
5051
- particleTextureIndex = 0.5 * (1.0 + position);
4867
+ const isVerticalLine = (annotation) =>
4868
+ Number.isFinite(annotation.x) && !('y' in annotation);
5052
4869
 
5053
- gl_Position = vec4(position, 0, 1);
5054
- }`;
4870
+ const isDomRect = (annotation) =>
4871
+ Number.isFinite(annotation.x) &&
4872
+ Number.isFinite(annotation.y) &&
4873
+ Number.isFinite(annotation.width) &&
4874
+ Number.isFinite(annotation.height);
5055
4875
 
5056
- const FRAGMENT_SHADER = `
5057
- precision highp float;
4876
+ const isRect = (annotation) =>
4877
+ Number.isFinite(annotation.x1) &&
4878
+ Number.isFinite(annotation.y1) &&
4879
+ Number.isFinite(annotation.x2) &&
4880
+ Number.isFinite(annotation.x2);
5058
4881
 
5059
- uniform float antiAliasing;
4882
+ const isPolygonAnnotation = (annotation) =>
4883
+ 'vertices' in annotation && annotation.vertices.length > 1;
5060
4884
 
5061
- varying vec4 color;
5062
- varying float finalPointSize;
4885
+ /**
4886
+ * Check if an array is a valid list of 2D vertices
4887
+ * @param {any} arg - The argument to check
4888
+ * @returns {boolean} True if argument is an array of [x, y] coordinate pairs
4889
+ */
4890
+ const isVertices = (arg) => {
4891
+ if (!Array.isArray(arg) || arg.length < 3) {
4892
+ return false;
4893
+ }
5063
4894
 
5064
- float linearstep(float edge0, float edge1, float x) {
5065
- return clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0);
5066
- }
4895
+ for (const vertex of arg) {
4896
+ if (
4897
+ !Array.isArray(vertex) ||
4898
+ vertex.length !== 2 ||
4899
+ typeof vertex[0] !== 'number' ||
4900
+ typeof vertex[1] !== 'number'
4901
+ ) {
4902
+ return false;
4903
+ }
4904
+ }
5067
4905
 
5068
- void main() {
5069
- vec2 c = gl_PointCoord * 2.0 - 1.0;
5070
- float sdf = length(c) * finalPointSize;
5071
- float alpha = linearstep(finalPointSize + antiAliasing, finalPointSize - antiAliasing, sdf);
4906
+ return true;
4907
+ };
5072
4908
 
5073
- gl_FragColor = vec4(color.rgb, alpha * color.a);
5074
- }
5075
- `;
4909
+ /**
4910
+ * Ensure a list of vertices forms a closed polygon
4911
+ * @param {Array<[number, number]>} vertices - Array of [x, y] coordinates
4912
+ * @returns {Array<[number, number]>} Closed polygon (first vertex repeated at end if needed)
4913
+ */
4914
+ const verticesToPolygon = (vertices) => {
4915
+ const polygon = [...vertices];
4916
+ const firstVertex = vertices.at(0);
4917
+ const lastVertex = vertices.at(-1);
4918
+ if (firstVertex[0] !== lastVertex[0] || firstVertex[1] !== lastVertex[1]) {
4919
+ polygon.push(firstVertex);
4920
+ }
4921
+ return polygon;
4922
+ };
5076
4923
 
5077
- const createVertexShader = (globalState) => `
5078
- precision highp float;
4924
+ const insertionSort = (array) => {
4925
+ const end = array.length;
4926
+ for (let i = 1; i < end; i++) {
4927
+ // Choosing the first element in our unsorted subarray
4928
+ const current = array[i];
4929
+ // The last element of our sorted subarray
4930
+ let j = i - 1;
4931
+ while (j > -1 && current < array[j]) {
4932
+ array[j + 1] = array[j];
4933
+ j--;
4934
+ }
4935
+ array[j + 1] = current;
4936
+ }
4937
+ return array;
4938
+ };
5079
4939
 
5080
- uniform sampler2D colorTex;
5081
- uniform float colorTexRes;
5082
- uniform float colorTexEps;
5083
- uniform sampler2D stateTex;
5084
- uniform float stateTexRes;
5085
- uniform float stateTexEps;
5086
- uniform float devicePixelRatio;
5087
- uniform sampler2D encodingTex;
5088
- uniform float encodingTexRes;
5089
- uniform float encodingTexEps;
5090
- uniform float pointSizeExtra;
5091
- uniform float pointOpacityMax;
5092
- uniform float pointOpacityScale;
5093
- uniform float numPoints;
5094
- uniform float globalState;
5095
- uniform float isColoredByZ;
5096
- uniform float isColoredByW;
5097
- uniform float isOpacityByZ;
5098
- uniform float isOpacityByW;
5099
- uniform float isOpacityByDensity;
5100
- uniform float isSizedByZ;
5101
- uniform float isSizedByW;
5102
- uniform float isPixelAligned;
5103
- uniform float colorMultiplicator;
5104
- uniform float opacityMultiplicator;
5105
- uniform float opacityDensity;
5106
- uniform float sizeMultiplicator;
5107
- uniform float numColorStates;
5108
- uniform float pointScale;
5109
- uniform float drawingBufferWidth;
5110
- uniform float drawingBufferHeight;
5111
- uniform mat4 modelViewProjection;
4940
+ const createRenderer = (
4941
+ /** @type {Partial<import('./types').RendererOptions>} */ options = {},
4942
+ ) => {
4943
+ let {
4944
+ regl,
4945
+ canvas = document.createElement('canvas'),
4946
+ gamma = DEFAULT_GAMMA,
4947
+ } = options;
5112
4948
 
5113
- attribute vec2 stateIndex;
4949
+ let isDestroyed = false;
5114
4950
 
5115
- varying vec4 color;
5116
- varying float finalPointSize;
4951
+ if (!regl) {
4952
+ regl = createRegl(canvas);
4953
+ }
5117
4954
 
5118
- void main() {
5119
- vec4 state = texture2D(stateTex, stateIndex);
4955
+ const isSupportingAllGlExtensions = checkReglExtensions(regl);
5120
4956
 
5121
- if (isPixelAligned < 0.5) {
5122
- gl_Position = modelViewProjection * vec4(state.x, state.y, 0.0, 1.0);
5123
- } else {
5124
- vec4 clipSpacePosition = modelViewProjection * vec4(state.x, state.y, 0.0, 1.0);
5125
- vec2 ndcPosition = clipSpacePosition.xy / clipSpacePosition.w;
5126
- vec2 pixelPos = 0.5 * (ndcPosition + 1.0) * vec2(drawingBufferWidth, drawingBufferHeight);
5127
- pixelPos = floor(pixelPos + 0.5); // Snap to nearest pixel
5128
- vec2 snappedPosition = (pixelPos / vec2(drawingBufferWidth, drawingBufferHeight)) * 2.0 - 1.0;
5129
- gl_Position = vec4(snappedPosition, 0.0, 1.0);
5130
- }
4957
+ const fboRes = [canvas.width, canvas.height];
4958
+ const fbo = regl.framebuffer({
4959
+ width: fboRes[0],
4960
+ height: fboRes[1],
4961
+ colorFormat: 'rgba',
4962
+ colorType: 'float',
4963
+ });
5131
4964
 
4965
+ /**
4966
+ * Render the float32 framebuffer to the internal canvas
4967
+ *
4968
+ * From https://observablehq.com/@rreusser/selecting-the-right-opacity-for-2d-point-clouds
4969
+ */
4970
+ const renderToCanvas = regl({
4971
+ vert: `
4972
+ precision highp float;
4973
+ attribute vec2 xy;
4974
+ void main () {
4975
+ gl_Position = vec4(xy, 0, 1);
4976
+ }`,
4977
+ frag: `
4978
+ precision highp float;
4979
+ uniform vec2 srcRes;
4980
+ uniform sampler2D src;
4981
+ uniform float gamma;
5132
4982
 
5133
- // Determine color index
5134
- float colorIndexZ = isColoredByZ * floor(state.z * colorMultiplicator);
5135
- float colorIndexW = isColoredByW * floor(state.w * colorMultiplicator);
4983
+ vec3 approxLinearToSRGB (vec3 rgb, float gamma) {
4984
+ return pow(clamp(rgb, vec3(0), vec3(1)), vec3(1.0 / gamma));
4985
+ }
5136
4986
 
5137
- // Multiply by the number of color states per color
5138
- // I.e., normal, active, hover, background, etc.
5139
- float colorIndex = (colorIndexZ + colorIndexW) * numColorStates;
4987
+ void main () {
4988
+ vec4 color = texture2D(src, gl_FragCoord.xy / srcRes);
4989
+ gl_FragColor = vec4(approxLinearToSRGB(color.rgb, gamma), color.a);
4990
+ }`,
4991
+ attributes: {
4992
+ xy: [-4, -4, 4, -4, 0, 4],
4993
+ },
4994
+ uniforms: {
4995
+ src: () => fbo,
4996
+ srcRes: () => fboRes,
4997
+ gamma: () => gamma,
4998
+ },
4999
+ count: 3,
5000
+ depth: { enable: false },
5001
+ blend: {
5002
+ enable: true,
5003
+ func: {
5004
+ srcRGB: 'one',
5005
+ srcAlpha: 'one',
5006
+ dstRGB: 'one minus src alpha',
5007
+ dstAlpha: 'one minus src alpha',
5008
+ },
5009
+ },
5010
+ });
5140
5011
 
5141
- // Half a "pixel" or "texel" in texture coordinates
5142
- float colorLinearIndex = colorIndex + globalState;
5012
+ /**
5013
+ * Copy the pixels from the internal canvas onto the target canvas
5014
+ */
5015
+ const copyTo = (targetCanvas) => {
5016
+ const ctx = targetCanvas.getContext('2d');
5017
+ ctx.clearRect(0, 0, targetCanvas.width, targetCanvas.height);
5018
+ ctx.drawImage(
5019
+ canvas,
5020
+ (canvas.width - targetCanvas.width) / 2,
5021
+ (canvas.height - targetCanvas.height) / 2,
5022
+ targetCanvas.width,
5023
+ targetCanvas.height,
5024
+ 0,
5025
+ 0,
5026
+ targetCanvas.width,
5027
+ targetCanvas.height,
5028
+ );
5029
+ };
5143
5030
 
5144
- // Need to add cEps here to avoid floating point issue that can lead to
5145
- // dramatic changes in which color is loaded as floor(3/2.9999) = 1 but
5146
- // floor(3/3.0001) = 0!
5147
- float colorRowIndex = floor((colorLinearIndex + colorTexEps) / colorTexRes);
5031
+ /**
5032
+ * The render function
5033
+ */
5034
+ const render = (
5035
+ /** @type {(): void} */ draw,
5036
+ /** @type {HTMLCanvasElement} */ targetCanvas,
5037
+ ) => {
5038
+ // Clear internal canvas
5039
+ regl.clear(CLEAR_OPTIONS);
5040
+ fbo.use(() => {
5041
+ // Clear framebuffer
5042
+ regl.clear(CLEAR_OPTIONS);
5043
+ draw();
5044
+ });
5045
+ renderToCanvas();
5046
+ copyTo(targetCanvas);
5047
+ };
5148
5048
 
5149
- vec2 colorTexIndex = vec2(
5150
- (colorLinearIndex / colorTexRes) - colorRowIndex + colorTexEps,
5151
- colorRowIndex / colorTexRes + colorTexEps
5152
- );
5049
+ /**
5050
+ * Update Regl's viewport, drawingBufferWidth, and drawingBufferHeight
5051
+ *
5052
+ * @description Call this method after the viewport has changed, e.g., width
5053
+ * or height have been altered
5054
+ */
5055
+ const refresh = () => {
5056
+ regl.poll();
5057
+ };
5153
5058
 
5154
- color = texture2D(colorTex, colorTexIndex);
5059
+ const drawFns = new Set();
5155
5060
 
5156
- // Retrieve point size
5157
- float pointSizeIndexZ = isSizedByZ * floor(state.z * sizeMultiplicator);
5158
- float pointSizeIndexW = isSizedByW * floor(state.w * sizeMultiplicator);
5159
- float pointSizeIndex = pointSizeIndexZ + pointSizeIndexW;
5061
+ /**
5062
+ * Register an draw function that is going to be invoked on every animation
5063
+ * frame.
5064
+ */
5065
+ const onFrame = (/** @type {(): void} */ draw) => {
5066
+ drawFns.add(draw);
5067
+ return () => {
5068
+ drawFns.delete(draw);
5069
+ };
5070
+ };
5160
5071
 
5161
- float pointSizeRowIndex = floor((pointSizeIndex + encodingTexEps) / encodingTexRes);
5162
- vec2 pointSizeTexIndex = vec2(
5163
- (pointSizeIndex / encodingTexRes) - pointSizeRowIndex + encodingTexEps,
5164
- pointSizeRowIndex / encodingTexRes + encodingTexEps
5165
- );
5166
- float pointSize = texture2D(encodingTex, pointSizeTexIndex).x;
5072
+ const frame = regl.frame(() => {
5073
+ const iterator = drawFns.values();
5074
+ let result = iterator.next();
5075
+ while (!result.done) {
5076
+ result.value(); // The draw function
5077
+ result = iterator.next();
5078
+ }
5079
+ });
5167
5080
 
5168
- // Retrieve opacity
5169
- ${
5170
- (() => {
5171
- // Drawing the inner border of selected points
5172
- if (globalState === 3) return '';
5081
+ const resize = (
5082
+ /** @type {number} */ customWidth,
5083
+ /** @type {number} */ customHeight,
5084
+ ) => {
5085
+ const width = customWidth == null ? window.innerWidth : customWidth;
5086
+ const height = customHeight == null ? window.innerHeight : customHeight;
5087
+ canvas.width = width * window.devicePixelRatio;
5088
+ canvas.height = height * window.devicePixelRatio;
5089
+ fboRes[0] = canvas.width;
5090
+ fboRes[1] = canvas.height;
5091
+ fbo.resize(...fboRes);
5092
+ };
5173
5093
 
5174
- // Draw points with opacity encoding or dynamic opacity
5175
- return `
5176
- if (isOpacityByDensity < 0.5) {
5177
- float opacityIndexZ = isOpacityByZ * floor(state.z * opacityMultiplicator);
5178
- float opacityIndexW = isOpacityByW * floor(state.w * opacityMultiplicator);
5179
- float opacityIndex = opacityIndexZ + opacityIndexW;
5094
+ const resizeHandler = () => {
5095
+ resize();
5096
+ };
5180
5097
 
5181
- float opacityRowIndex = floor((opacityIndex + encodingTexEps) / encodingTexRes);
5182
- vec2 opacityTexIndex = vec2(
5183
- (opacityIndex / encodingTexRes) - opacityRowIndex + encodingTexEps,
5184
- opacityRowIndex / encodingTexRes + encodingTexEps
5185
- );
5186
- color.a = texture2D(encodingTex, opacityTexIndex)[${1 + globalState}];
5187
- } else {
5188
- color.a = min(1.0, opacityDensity + globalState);
5189
- }
5190
- `;
5191
- })()
5098
+ if (!options.canvas) {
5099
+ window.addEventListener('resize', resizeHandler);
5100
+ window.addEventListener('orientationchange', resizeHandler);
5101
+ resize();
5192
5102
  }
5193
5103
 
5194
- color.a = min(pointOpacityMax, color.a) * pointOpacityScale;
5195
- finalPointSize = (pointSize * pointScale) + pointSizeExtra;
5196
- gl_PointSize = finalPointSize;
5197
- }
5198
- `;
5104
+ /**
5105
+ * Destroy the renderer to free resources and cancel animation frames
5106
+ */
5107
+ const destroy = () => {
5108
+ isDestroyed = true;
5109
+ window.removeEventListener('resize', resizeHandler);
5110
+ window.removeEventListener('orientationchange', resizeHandler);
5111
+ frame.cancel();
5112
+ canvas = undefined;
5113
+ regl.destroy();
5114
+ regl = undefined;
5115
+ };
5116
+
5117
+ return {
5118
+ /**
5119
+ * Get the associated canvas element
5120
+ * @return {HTMLCanvasElement} The associated canvas element
5121
+ */
5122
+ get canvas() {
5123
+ return canvas;
5124
+ },
5125
+ /**
5126
+ * Get the associated Regl instance
5127
+ * @return {import('regl').Regl} The associated Regl instance
5128
+ */
5129
+ get regl() {
5130
+ return regl;
5131
+ },
5132
+ /**
5133
+ * Get the gamma value
5134
+ * @return {number} The gamma value
5135
+ */
5136
+ get gamma() {
5137
+ return gamma;
5138
+ },
5139
+ /**
5140
+ * Set gamma to a new value
5141
+ * @param {number} newGamma - The new gamma value
5142
+ */
5143
+ set gamma(newGamma) {
5144
+ gamma = +newGamma;
5145
+ },
5146
+ /**
5147
+ * Get whether the browser supports all necessary WebGL features
5148
+ * @return {boolean} If `true` the browser supports all necessary WebGL features
5149
+ */
5150
+ get isSupported() {
5151
+ return isSupportingAllGlExtensions;
5152
+ },
5153
+ /**
5154
+ * Get whether the renderer (and its Regl instance) is destroyed
5155
+ * @return {boolean} If `true` the renderer is destroyed
5156
+ */
5157
+ get isDestroyed() {
5158
+ return isDestroyed;
5159
+ },
5160
+ render,
5161
+ resize,
5162
+ onFrame,
5163
+ refresh,
5164
+ destroy,
5165
+ };
5166
+ };
5199
5167
 
5200
5168
  /* eslint-env worker */
5201
5169
  /* eslint no-restricted-globals: 1 */
@@ -5304,7 +5272,6 @@ const worker = function worker() {
5304
5272
  * @param {[type]} simplified [description]
5305
5273
  * @return {[type]} [description]
5306
5274
  */
5307
- // biome-ignore lint/style/useNamingConvention: DP stands for Douglas Peucker
5308
5275
  const simplifyDPStep = (points, first, last, tolerance, simplified) => {
5309
5276
  let maxDist = tolerance;
5310
5277
  let index;
@@ -5412,7 +5379,6 @@ const worker = function worker() {
5412
5379
  const groupedPoints = {};
5413
5380
 
5414
5381
  const isOrdered = !Number.isNaN(+points[0][5]);
5415
- // biome-ignore lint/complexity/noForEach: somehow for .. of does not work in a worker
5416
5382
  points.forEach((point) => {
5417
5383
  const segId = point[4];
5418
5384
 
@@ -5428,7 +5394,6 @@ const worker = function worker() {
5428
5394
  });
5429
5395
 
5430
5396
  // The filtering ensures that non-existing array entries are removed
5431
- // biome-ignore lint/complexity/noForEach: somehow for .. of does not work in a worker
5432
5397
  Object.entries(groupedPoints).forEach((idPoints) => {
5433
5398
  groupedPoints[idPoints[0]] = idPoints[1].filter((v) => v);
5434
5399
  // Store the first point as the reference
@@ -5485,8 +5450,6 @@ const createSplineCurve = (
5485
5450
  worker$1.postMessage({ points, options });
5486
5451
  });
5487
5452
 
5488
- var version = "1.14.1+1.0.1";
5489
-
5490
5453
  const deprecations = {
5491
5454
  showRecticle: {
5492
5455
  replacement: 'showReticle',
@@ -5595,8 +5558,8 @@ const createScatterplot = (
5595
5558
  deselectOnEscape = DEFAULT_DESELECT_ON_ESCAPE,
5596
5559
  lassoColor = DEFAULT_LASSO_COLOR,
5597
5560
  lassoLineWidth = DEFAULT_LASSO_LINE_WIDTH,
5598
- lassoMinDelay = DEFAULT_LASSO_MIN_DELAY,
5599
- lassoMinDist = DEFAULT_LASSO_MIN_DIST,
5561
+ lassoMinDelay = DEFAULT_LASSO_MIN_DELAY$1,
5562
+ lassoMinDist = DEFAULT_LASSO_MIN_DIST$1,
5600
5563
  lassoMode = DEFAULT_LASSO_MODE,
5601
5564
  lassoClearEvent = DEFAULT_LASSO_CLEAR_EVENT,
5602
5565
  lassoInitiator = DEFAULT_LASSO_INITIATOR,
@@ -5607,7 +5570,7 @@ const createScatterplot = (
5607
5570
  lassoLongPressAfterEffectTime = DEFAULT_LASSO_LONG_PRESS_AFTER_EFFECT_TIME,
5608
5571
  lassoLongPressEffectDelay = DEFAULT_LASSO_LONG_PRESS_EFFECT_DELAY,
5609
5572
  lassoLongPressRevertEffectTime = DEFAULT_LASSO_LONG_PRESS_REVERT_EFFECT_TIME,
5610
- lassoType = DEFAULT_LASSO_TYPE,
5573
+ lassoType = DEFAULT_LASSO_TYPE$1,
5611
5574
  lassoBrushSize = DEFAULT_LASSO_BRUSH_SIZE,
5612
5575
  actionKeyMap = DEFAULT_ACTION_KEY_MAP,
5613
5576
  mouseMode = DEFAULT_MOUSE_MODE,
@@ -5639,6 +5602,7 @@ const createScatterplot = (
5639
5602
  opacityInactiveMax = DEFAULT_OPACITY_INACTIVE_MAX,
5640
5603
  opacityInactiveScale = DEFAULT_OPACITY_INACTIVE_SCALE,
5641
5604
  sizeBy = DEFAULT_SIZE_BY,
5605
+ pointOrder = DEFAULT_POINT_ORDER,
5642
5606
  pointScaleMode = DEFAULT_POINT_SCALE_MODE,
5643
5607
  height = DEFAULT_HEIGHT,
5644
5608
  width = DEFAULT_WIDTH,
@@ -5714,9 +5678,7 @@ const createScatterplot = (
5714
5678
  let pointConnections;
5715
5679
  let pointConnectionMap;
5716
5680
  let computingPointConnectionCurves;
5717
- // biome-ignore lint/style/useNamingConvention: HLine stands for HorizontalLine
5718
5681
  let reticleHLine;
5719
- // biome-ignore lint/style/useNamingConvention: VLine stands for VerticalLine
5720
5682
  let reticleVLine;
5721
5683
  let computedPointSizeMouseDetection;
5722
5684
  let lassoInitiatorTimeout;
@@ -5849,6 +5811,8 @@ const createScatterplot = (
5849
5811
  let normalPointsIndexBuffer; // Buffer holding the indices pointing to the correct texel
5850
5812
  let selectedPointsIndexBuffer; // Used for pointing to the selected texels
5851
5813
  let hoveredPointIndexBuffer; // Used for pointing to the hovered texels
5814
+ let pointOrderIndices = null; // Validated ordered point indices (Int32Array or null)
5815
+ let pointOrderIndex = null; // Float32Array of tex coords in pointOrder sequence
5852
5816
 
5853
5817
  let cameraZoomTargetStart; // Stores the start (i.e., current) camera target for zooming
5854
5818
  let cameraZoomTargetEnd; // Stores the end camera target for zooming
@@ -5871,9 +5835,7 @@ const createScatterplot = (
5871
5835
  let isAnnotationsDrawn = false;
5872
5836
  let isMouseOverCanvasChecked = false;
5873
5837
 
5874
- // biome-ignore lint/style/useNamingConvention: ZDate is not one word
5875
5838
  let valueZDataType = CATEGORICAL;
5876
- // biome-ignore lint/style/useNamingConvention: WDate is not one word
5877
5839
  let valueWDataType = CATEGORICAL;
5878
5840
 
5879
5841
  /** @type{number|undefined} */
@@ -5947,9 +5909,7 @@ const createScatterplot = (
5947
5909
  return points;
5948
5910
  };
5949
5911
 
5950
- // biome-ignore lint/style/useNamingConvention: BBox stands for BoundingBox
5951
5912
  const getPointsInBBox = (x0, y0, x1, y1) => {
5952
- // biome-ignore lint/style/useNamingConvention: BBox stands for BoundingBox
5953
5913
  const pointsInBBox = spatialIndex.range(x0, y0, x1, y1);
5954
5914
  if (isPointsFiltered) {
5955
5915
  return pointsInBBox.filter((i) => filteredPointsSet.has(i));
@@ -5964,7 +5924,6 @@ const createScatterplot = (
5964
5924
  const pointSizeNdc = getPointSizeNdc(4);
5965
5925
 
5966
5926
  // Get all points within a close range
5967
- // biome-ignore lint/style/useNamingConvention: BBox stands for BoundingBox
5968
5927
  const pointsInBBox = getPointsInBBox(
5969
5928
  xNdc - pointSizeNdc,
5970
5929
  yNdc - pointSizeNdc,
@@ -6002,7 +5961,6 @@ const createScatterplot = (
6002
5961
  }
6003
5962
 
6004
5963
  // ...to efficiently preselect potentially selected points
6005
- // biome-ignore lint/style/useNamingConvention: BBox stands for BoundingBox
6006
5964
  const pointsInBBox = getPointsInBBox(...bBox);
6007
5965
  // next we test each point in the bounding box if it is in the polygon too
6008
5966
  const pointsInPolygon = [];
@@ -6107,6 +6065,40 @@ const createScatterplot = (
6107
6065
  }
6108
6066
  };
6109
6067
 
6068
+ /**
6069
+ * Convert vertices from data space to GL space
6070
+ * @param {Array<[number, number]>} vertices - Vertices in data space
6071
+ * @returns {number[] | null} Flat array of GL coordinates or null if scales not defined
6072
+ */
6073
+ const verticesFromDataToGl = (vertices) => {
6074
+ // Check if xScale/yScale are defined
6075
+ if (!(xScale && yScale)) {
6076
+ // biome-ignore lint/suspicious/noConsole: User warning for missing configuration
6077
+ console.warn(
6078
+ 'xScale and yScale must be defined for programmatic lasso selection',
6079
+ );
6080
+ return null;
6081
+ }
6082
+
6083
+ const verticesGl = [];
6084
+ for (const [x, y] of vertices) {
6085
+ // Step 1: Data space → normalized [0, 1]
6086
+ const xNorm = (x - xDomainStart) / xDomainSize;
6087
+ const yNorm = (y - yDomainStart) / yDomainSize;
6088
+
6089
+ // Step 2: Normalized [0, 1] → NDC [-1, 1]
6090
+ const xNdc = xNorm * 2 - 1;
6091
+ const yNdc = yNorm * 2 - 1;
6092
+
6093
+ // Step 3: NDC → GL space (camera transform)
6094
+ const [xGl, yGl] = getScatterGlPos(xNdc, yNdc);
6095
+
6096
+ verticesGl.push(xGl, yGl);
6097
+ }
6098
+
6099
+ return verticesGl;
6100
+ };
6101
+
6110
6102
  /**
6111
6103
  * Select and highlight a set of points
6112
6104
  * @param {number | number[]} pointIdxs
@@ -6204,12 +6196,53 @@ const createScatterplot = (
6204
6196
  setPointConnectionColorState(selectedPoints, 1);
6205
6197
 
6206
6198
  if (!preventEvent) {
6207
- pubSub.publish('select', { points: selectedPoints });
6199
+ pubSub.publish('select', { points: selectedPoints, lassoMode });
6208
6200
  }
6209
6201
 
6210
6202
  draw = true;
6211
6203
  };
6212
6204
 
6205
+ /**
6206
+ * Lasso a certain area and select contained points
6207
+ * @param {[number, number][]} vertices - Lasso vertices in either data space (default) or GL space
6208
+ * @param {import('./types').ScatterplotMethodOptions['lasso']}
6209
+ */
6210
+ const lassoSelect = (
6211
+ vertices,
6212
+ { merge = false, remove = false, isGl = false } = {},
6213
+ ) => {
6214
+ if (!isVertices(vertices)) {
6215
+ throw new Error(
6216
+ 'Lasso selection requires at least 3 vertices as [x, y] coordinate pairs',
6217
+ );
6218
+ }
6219
+
6220
+ const closedPolygon = verticesToPolygon(vertices);
6221
+
6222
+ let polygonGl;
6223
+ let polygonGlFlat;
6224
+
6225
+ if (isGl) {
6226
+ polygonGl = closedPolygon;
6227
+ polygonGlFlat = closedPolygon.flat();
6228
+ } else {
6229
+ polygonGlFlat = verticesFromDataToGl(closedPolygon);
6230
+
6231
+ if (!polygonGlFlat) {
6232
+ throw new Error(
6233
+ 'xScale and yScale must be defined to convert lasso vertices from data space to GL space',
6234
+ );
6235
+ }
6236
+
6237
+ polygonGl = [];
6238
+ for (let i = 0; i < polygonGlFlat.length; i += 2) {
6239
+ polygonGl.push([polygonGlFlat[i], polygonGlFlat[i + 1]]);
6240
+ }
6241
+ }
6242
+
6243
+ lassoEnd(polygonGl, polygonGlFlat, { merge, remove });
6244
+ };
6245
+
6213
6246
  /**
6214
6247
  * @param {number} point
6215
6248
  * @param {import('./types').ScatterplotMethodOptions['hover']} options
@@ -6886,6 +6919,44 @@ const createScatterplot = (
6886
6919
  const setSizeBy = (type) => {
6887
6920
  sizeBy = getEncodingType(type, DEFAULT_SIZE_BY);
6888
6921
  };
6922
+ const setPointOrder = (newPointOrder) => {
6923
+ if (newPointOrder === null || newPointOrder === undefined) {
6924
+ pointOrder = null;
6925
+ } else if (Array.isArray(newPointOrder) || ArrayBuffer.isView(newPointOrder)) {
6926
+ pointOrder = newPointOrder;
6927
+ } else {
6928
+ return;
6929
+ }
6930
+
6931
+ if (isPointsDrawn) {
6932
+ computePointOrderIndex();
6933
+ if (isPointsFiltered) {
6934
+ // Re-apply filter respecting the new point order
6935
+ const filteredPointsBuffer = [];
6936
+ if (pointOrderIndices !== null) {
6937
+ for (let i = 0; i < pointOrderIndices.length; i++) {
6938
+ if (filteredPointsSet.has(pointOrderIndices[i])) {
6939
+ filteredPointsBuffer.push.apply(
6940
+ filteredPointsBuffer,
6941
+ indexToStateTexCoord(pointOrderIndices[i]),
6942
+ );
6943
+ }
6944
+ }
6945
+ } else {
6946
+ const sortedFiltered = insertionSort([...filteredPointsSet]);
6947
+ for (const idx of sortedFiltered) {
6948
+ filteredPointsBuffer.push.apply(
6949
+ filteredPointsBuffer,
6950
+ indexToStateTexCoord(idx),
6951
+ );
6952
+ }
6953
+ }
6954
+ normalPointsIndexBuffer.subdata(filteredPointsBuffer);
6955
+ } else {
6956
+ normalPointsIndexBuffer.subdata(getEffectivePointIndex(numPoints));
6957
+ }
6958
+ }
6959
+ };
6889
6960
  const setPointConnectionColorBy = (type) => {
6890
6961
  pointConnectionColorBy = getEncodingType(
6891
6962
  type,
@@ -7053,16 +7124,14 @@ const createScatterplot = (
7053
7124
  getPointOpacityScale = getPointOpacityScaleBase,
7054
7125
  ) =>
7055
7126
  renderer.regl({
7056
- frag: renderPointsAsSquares ? FRAGMENT_SHADER$1 : FRAGMENT_SHADER,
7127
+ frag: renderPointsAsSquares ? FRAGMENT_SHADER : FRAGMENT_SHADER$1,
7057
7128
  vert: createVertexShader(globalState),
7058
7129
 
7059
7130
  blend: {
7060
7131
  enable: !disableAlphaBlending,
7061
7132
  func: {
7062
- // biome-ignore lint/style/useNamingConvention: Regl specific
7063
7133
  srcRGB: 'src alpha',
7064
7134
  srcAlpha: 'one',
7065
- // biome-ignore lint/style/useNamingConvention: Regl specific
7066
7135
  dstRGB: 'one minus src alpha',
7067
7136
  dstAlpha: 'one minus src alpha',
7068
7137
  },
@@ -7203,10 +7272,8 @@ const createScatterplot = (
7203
7272
  blend: {
7204
7273
  enable: true,
7205
7274
  func: {
7206
- // biome-ignore lint/style/useNamingConvention: Regl specific
7207
7275
  srcRGB: 'src alpha',
7208
7276
  srcAlpha: 'one',
7209
- // biome-ignore lint/style/useNamingConvention: Regl specific
7210
7277
  dstRGB: 'one minus src alpha',
7211
7278
  dstAlpha: 'one minus src alpha',
7212
7279
  },
@@ -7284,6 +7351,51 @@ const createScatterplot = (
7284
7351
  return index;
7285
7352
  };
7286
7353
 
7354
+ const computePointOrderIndex = () => {
7355
+ if (pointOrder === null) {
7356
+ pointOrderIndices = null;
7357
+ pointOrderIndex = null;
7358
+ return;
7359
+ }
7360
+
7361
+ const includedSet = new Set();
7362
+ const orderedIndices = [];
7363
+
7364
+ for (let i = 0; i < pointOrder.length; i++) {
7365
+ const idx = pointOrder[i];
7366
+ if (
7367
+ Number.isFinite(idx) &&
7368
+ idx >= 0 &&
7369
+ idx < numPoints &&
7370
+ !includedSet.has(idx)
7371
+ ) {
7372
+ orderedIndices.push(idx);
7373
+ includedSet.add(idx);
7374
+ }
7375
+ }
7376
+
7377
+ // Append any missing indices in sequential order
7378
+ for (let i = 0; i < numPoints; i++) {
7379
+ if (!includedSet.has(i)) {
7380
+ orderedIndices.push(i);
7381
+ }
7382
+ }
7383
+
7384
+ pointOrderIndices = orderedIndices;
7385
+
7386
+ pointOrderIndex = new Float32Array(orderedIndices.length * 2);
7387
+ let j = 0;
7388
+ for (let i = 0; i < orderedIndices.length; i++) {
7389
+ const texCoord = indexToStateTexCoord(orderedIndices[i]);
7390
+ pointOrderIndex[j] = texCoord[0];
7391
+ pointOrderIndex[j + 1] = texCoord[1];
7392
+ j += 2;
7393
+ }
7394
+ };
7395
+
7396
+ const getEffectivePointIndex = (count) =>
7397
+ pointOrderIndex !== null ? pointOrderIndex : createPointIndex(count);
7398
+
7287
7399
  const createStateTexture = (newPoints, dataTypes = {}) => {
7288
7400
  const numNewPoints = newPoints.length;
7289
7401
  stateTexRes = Math.max(2, Math.ceil(Math.sqrt(numNewPoints)));
@@ -7375,9 +7487,18 @@ const createScatterplot = (
7375
7487
  const preventFilterReset =
7376
7488
  options?.preventFilterReset && newPoints.length === numPoints;
7377
7489
 
7490
+ const prevNumPoints = numPoints;
7378
7491
  numPoints = newPoints.length;
7379
7492
  numPointsInView = numPoints;
7380
7493
 
7494
+ // Reset pointOrder when re-drawing with different-length data
7495
+ // (ordering becomes meaningless). Skip on first draw (prevNumPoints === 0).
7496
+ if (prevNumPoints > 0 && numPoints !== prevNumPoints) {
7497
+ pointOrder = null;
7498
+ pointOrderIndices = null;
7499
+ pointOrderIndex = null;
7500
+ }
7501
+
7381
7502
  if (stateTex) {
7382
7503
  stateTex.destroy();
7383
7504
  }
@@ -7387,10 +7508,11 @@ const createScatterplot = (
7387
7508
  });
7388
7509
 
7389
7510
  if (!preventFilterReset) {
7511
+ computePointOrderIndex();
7390
7512
  normalPointsIndexBuffer({
7391
7513
  usage: 'static',
7392
7514
  type: 'float',
7393
- data: createPointIndex(numPoints),
7515
+ data: getEffectivePointIndex(numPoints),
7394
7516
  });
7395
7517
  }
7396
7518
 
@@ -7631,7 +7753,7 @@ const createScatterplot = (
7631
7753
  const unfilter = ({ preventEvent = false } = {}) => {
7632
7754
  isPointsFiltered = false;
7633
7755
  filteredPointsSet.clear();
7634
- normalPointsIndexBuffer.subdata(createPointIndex(numPoints));
7756
+ normalPointsIndexBuffer.subdata(getEffectivePointIndex(numPoints));
7635
7757
 
7636
7758
  return new Promise((resolve) => {
7637
7759
  const finish = () => {
@@ -7690,9 +7812,20 @@ const createScatterplot = (
7690
7812
  }
7691
7813
  }
7692
7814
 
7693
- const sortedFilteredPoints = insertionSort([...filteredPoints]);
7815
+ let orderedFilteredPoints;
7816
+ if (pointOrderIndices !== null) {
7817
+ // Maintain the custom point order within the filtered set
7818
+ orderedFilteredPoints = [];
7819
+ for (let i = 0; i < pointOrderIndices.length; i++) {
7820
+ if (filteredPointsSet.has(pointOrderIndices[i])) {
7821
+ orderedFilteredPoints.push(pointOrderIndices[i]);
7822
+ }
7823
+ }
7824
+ } else {
7825
+ orderedFilteredPoints = insertionSort([...filteredPoints]);
7826
+ }
7694
7827
 
7695
- for (const pointIdx of sortedFilteredPoints) {
7828
+ for (const pointIdx of orderedFilteredPoints) {
7696
7829
  filteredPointsBuffer.push.apply(
7697
7830
  filteredPointsBuffer,
7698
7831
  indexToStateTexCoord(pointIdx),
@@ -8079,7 +8212,7 @@ const createScatterplot = (
8079
8212
  continue;
8080
8213
  }
8081
8214
 
8082
- if (isPolygon(annotation)) {
8215
+ if (isPolygonAnnotation(annotation)) {
8083
8216
  newPoints.push(annotation.vertices.flatMap(identity));
8084
8217
  addColorAndWidth(annotation);
8085
8218
  }
@@ -8120,7 +8253,6 @@ const createScatterplot = (
8120
8253
  * @param {number[]} pointIdxs - A list of point indices
8121
8254
  * @returns {import('./types').Rect} The bounding box
8122
8255
  */
8123
- // biome-ignore lint/style/useNamingConvention: BBox stands for BoundingBox
8124
8256
  const getBBoxOfPoints = (pointIdxs) => {
8125
8257
  let xMin = Number.POSITIVE_INFINITY;
8126
8258
  let xMax = Number.NEGATIVE_INFINITY;
@@ -8156,7 +8288,6 @@ const createScatterplot = (
8156
8288
  // Vertical field of view
8157
8289
  // The Arc Tangent is based on the original camera position. Otherwise
8158
8290
  // we would have to do `Math.atan(1 / camera.view[5])`
8159
- // biome-ignore lint/style/useNamingConvention: FOV stands for field of view
8160
8291
  const vFOV = 2 * Math.atan(1);
8161
8292
 
8162
8293
  const aspectRatio = viewAspectRatio / dataAspectRatio;
@@ -8556,7 +8687,6 @@ const createScatterplot = (
8556
8687
  reticleVLine.setStyle({ color: reticleColor });
8557
8688
  };
8558
8689
 
8559
- // biome-ignore lint/style/useNamingConvention: XScale are two words
8560
8690
  const setXScale = (newXScale) => {
8561
8691
  if (!newXScale) {
8562
8692
  return;
@@ -8569,7 +8699,6 @@ const createScatterplot = (
8569
8699
  updateScales();
8570
8700
  };
8571
8701
 
8572
- // biome-ignore lint/style/useNamingConvention: YScale are two words
8573
8702
  const setYScale = (newYScale) => {
8574
8703
  if (!newYScale) {
8575
8704
  return;
@@ -8743,7 +8872,6 @@ const createScatterplot = (
8743
8872
  annotationLineWidth = +newAnnotationLineWidth;
8744
8873
  };
8745
8874
 
8746
- // biome-ignore lint/style/useNamingConvention: HVLine stands for horizontal vertical line
8747
8875
  const setAnnotationHVLineLimit = (newAnnotationHVLineLimit) => {
8748
8876
  annotationHVLineLimit = +newAnnotationHVLineLimit;
8749
8877
  };
@@ -8820,6 +8948,10 @@ const createScatterplot = (
8820
8948
  return sizeBy;
8821
8949
  }
8822
8950
 
8951
+ if (property === 'pointOrder') {
8952
+ return pointOrder !== null ? [...pointOrder] : null;
8953
+ }
8954
+
8823
8955
  if (property === 'deselectOnDblClick') {
8824
8956
  return deselectOnDblClick;
8825
8957
  }
@@ -9206,6 +9338,10 @@ const createScatterplot = (
9206
9338
  setSizeBy(properties.sizeBy);
9207
9339
  }
9208
9340
 
9341
+ if (properties.pointOrder !== undefined) {
9342
+ setPointOrder(properties.pointOrder);
9343
+ }
9344
+
9209
9345
  if (properties.opacity !== undefined) {
9210
9346
  setOpacity(properties.opacity);
9211
9347
  }
@@ -9903,6 +10039,9 @@ const createScatterplot = (
9903
10039
  init();
9904
10040
 
9905
10041
  return {
10042
+ attachSpatialIndex: (buffer) => {
10043
+ spatialIndex = kdbushFrom(buffer);
10044
+ },
9906
10045
  /**
9907
10046
  * Get whether the browser supports all necessary WebGL features
9908
10047
  * @return {boolean} If `true` the browser supports all necessary WebGL features
@@ -9926,6 +10065,7 @@ const createScatterplot = (
9926
10065
  get,
9927
10066
  getScreenPosition,
9928
10067
  hover,
10068
+ lassoSelect,
9929
10069
  redraw,
9930
10070
  refresh: renderer.refresh,
9931
10071
  reset: withDraw(reset),