@hema-to/regl-scatterplot 1.14.1-hemato.3 → 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};` +
@@ -2840,232 +2993,58 @@ const createWorker = (fn) => {
2840
2993
  // Clean up URL
2841
2994
  URL.revokeObjectURL(workerUrl);
2842
2995
 
2843
- return worker;
2844
- };
2845
-
2846
- /**
2847
- * Create KDBush from an either point data or an existing spatial index
2848
- * @param {import('./types').Points | ArrayBuffer} pointsOrIndex - Points or KDBush index
2849
- * @param {Partial<import('./types').CreateKDBushOptions>} options - Options for configuring the index and its creation
2850
- * @return {Promise<KDBush>} KDBush instance
2851
- */
2852
- const createKdbush = (
2853
- pointsOrIndex,
2854
- options = { nodeSize: 16, useWorker: undefined },
2855
- ) =>
2856
- new Promise((resolve, reject) => {
2857
- if (pointsOrIndex instanceof ArrayBuffer) {
2858
- resolve(KDBush$1.from(pointsOrIndex));
2859
- } else if (
2860
- (pointsOrIndex.length < WORKER_THRESHOLD ||
2861
- options.useWorker === false) &&
2862
- options.useWorker !== true
2863
- ) {
2864
- const index = new KDBush$1(pointsOrIndex.length, options.nodeSize);
2865
- for (const pointOrIndex of pointsOrIndex) {
2866
- index.add(pointOrIndex[0], pointOrIndex[1]);
2867
- }
2868
- index.finish();
2869
- resolve(index);
2870
- } else {
2871
- const worker = createWorker(workerFn);
2872
-
2873
- worker.onmessage = (e) => {
2874
- if (e.data.error) {
2875
- reject(e.data.error);
2876
- } else {
2877
- resolve(KDBush$1.from(e.data));
2878
- }
2879
- worker.terminate();
2880
- };
2881
-
2882
- worker.postMessage({ points: pointsOrIndex, nodeSize: options.nodeSize });
2883
- }
2884
- });
2885
-
2886
- 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';
2890
- const DEFAULT_BRUSH_SIZE = 24;
2891
- const LASSO_SHOW_START_INITIATOR_TIME = 2500;
2892
- const LASSO_HIDE_START_INITIATOR_TIME = 250;
2893
-
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;
2996
+ return worker;
2997
+ };
3023
2998
 
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
- ]);
2999
+ /**
3000
+ * Create KDBush from an either point data or an existing spatial index
3001
+ * @param {import('./types').Points | ArrayBuffer} pointsOrIndex - Points or KDBush index
3002
+ * @param {Partial<import('./types').CreateKDBushOptions>} options - Options for configuring the index and its creation
3003
+ * @return {Promise<KDBush>} KDBush instance
3004
+ */
3005
+ const createKdbush = (
3006
+ pointsOrIndex,
3007
+ options = { nodeSize: 16, useWorker: undefined },
3008
+ ) =>
3009
+ new Promise((resolve, reject) => {
3010
+ if (pointsOrIndex instanceof ArrayBuffer) {
3011
+ resolve(KDBush$1.from(pointsOrIndex));
3012
+ } else if (
3013
+ (pointsOrIndex.length < WORKER_THRESHOLD ||
3014
+ options.useWorker === false) &&
3015
+ options.useWorker !== true
3016
+ ) {
3017
+ const index = new KDBush$1(pointsOrIndex.length, options.nodeSize);
3018
+ for (const pointOrIndex of pointsOrIndex) {
3019
+ index.add(pointOrIndex[0], pointOrIndex[1]);
3020
+ }
3021
+ index.finish();
3022
+ resolve(index);
3023
+ } else {
3024
+ const worker = createWorker(workerFn);
3035
3025
 
3036
- // Error codes
3037
- const IMAGE_LOAD_ERROR = 'IMAGE_LOAD_ERROR';
3026
+ worker.onmessage = (e) => {
3027
+ if (e.data.error) {
3028
+ reject(e.data.error);
3029
+ } else {
3030
+ resolve(KDBush$1.from(e.data));
3031
+ }
3032
+ worker.terminate();
3033
+ };
3038
3034
 
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
- );
3035
+ worker.postMessage({ points: pointsOrIndex, nodeSize: options.nodeSize });
3036
+ }
3037
+ });
3063
3038
 
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.';
3039
+ const kdbushFrom = (buffer) => KDBush$1.from(buffer);
3040
+
3041
+ const DEFAULT_LASSO_START_INITIATOR_SHOW = true;
3042
+ const DEFAULT_LASSO_MIN_DELAY = 8;
3043
+ const DEFAULT_LASSO_MIN_DIST = 2;
3044
+ const DEFAULT_LASSO_TYPE = 'freeform';
3045
+ const DEFAULT_BRUSH_SIZE = 24;
3046
+ const LASSO_SHOW_START_INITIATOR_TIME = 2500;
3047
+ const LASSO_HIDE_START_INITIATOR_TIME = 250;
3069
3048
 
3070
3049
  const getInTime = (p, time, extraTime) => (1 - p) * time + extraTime;
3071
3050
 
@@ -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
  ) => {
@@ -4098,1086 +4077,1093 @@ const createLasso = (
4098
4077
  newInitiatorParentElement !== null &&
4099
4078
  newInitiatorParentElement !== initiatorParentElement
4100
4079
  ) {
4101
- initiatorParentElement.removeChild(initiator);
4102
- newInitiatorParentElement.appendChild(initiator);
4103
- initiatorParentElement = newInitiatorParentElement;
4104
- }
4105
-
4106
- if (
4107
- newLongPressIndicatorParentElement !== null &&
4108
- newLongPressIndicatorParentElement !== longPressIndicatorParentElement
4109
- ) {
4110
- longPressIndicatorParentElement.removeChild(longPress);
4111
- newLongPressIndicatorParentElement.appendChild(longPress);
4112
- longPressIndicatorParentElement = newLongPressIndicatorParentElement;
4113
- }
4114
-
4115
- if (enableInitiator) {
4116
- initiator.addEventListener('click', initiatorClickHandler);
4117
- initiator.addEventListener('mousedown', initiatorMouseDownHandler);
4118
- initiator.addEventListener('mouseleave', initiatorMouseLeaveHandler);
4119
- } else {
4120
- initiator.removeEventListener('mousedown', initiatorMouseDownHandler);
4121
- initiator.removeEventListener('mouseleave', initiatorMouseLeaveHandler);
4122
- }
4123
-
4124
- if (newType !== null) {
4125
- setExtendLasso(newType);
4126
- }
4127
- };
4128
-
4129
- const destroy = () => {
4130
- initiatorParentElement.removeChild(initiator);
4131
- longPressIndicatorParentElement.removeChild(longPress);
4132
- window.removeEventListener('mouseup', mouseUpHandler);
4133
- initiator.removeEventListener('click', initiatorClickHandler);
4134
- initiator.removeEventListener('mousedown', initiatorMouseDownHandler);
4135
- initiator.removeEventListener('mouseleave', initiatorMouseLeaveHandler);
4136
- };
4137
-
4138
- const withPublicMethods = () => (self) =>
4139
- assign(self, {
4140
- clear,
4141
- destroy,
4142
- end,
4143
- extend: extendPublic,
4144
- get,
4145
- set,
4146
- showInitiator,
4147
- hideInitiator,
4148
- showLongPressIndicator,
4149
- hideLongPressIndicator,
4150
- });
4151
-
4152
- 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
- };
4196
-
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,
4206
- });
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
-
4222
- return createOriginalRegl({ gl, extensions });
4223
- };
4224
-
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);
4235
-
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;
4248
-
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
- }
4255
-
4256
- return [xMin, yMin, xMax, yMax];
4257
- };
4258
-
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;
4272
-
4273
- const REGEX_HEX_TO_RGB = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
4080
+ initiatorParentElement.removeChild(initiator);
4081
+ newInitiatorParentElement.appendChild(initiator);
4082
+ initiatorParentElement = newInitiatorParentElement;
4083
+ }
4274
4084
 
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);
4085
+ if (
4086
+ newLongPressIndicatorParentElement !== null &&
4087
+ newLongPressIndicatorParentElement !== longPressIndicatorParentElement
4088
+ ) {
4089
+ longPressIndicatorParentElement.removeChild(longPress);
4090
+ newLongPressIndicatorParentElement.appendChild(longPress);
4091
+ longPressIndicatorParentElement = newLongPressIndicatorParentElement;
4092
+ }
4288
4093
 
4289
- const isConditionalArray = (a, condition, { minLength = 0 } = {}) =>
4290
- Array.isArray(a) && a.length >= minLength && a.every(condition);
4094
+ if (enableInitiator) {
4095
+ initiator.addEventListener('click', initiatorClickHandler);
4096
+ initiator.addEventListener('mousedown', initiatorMouseDownHandler);
4097
+ initiator.addEventListener('mouseleave', initiatorMouseLeaveHandler);
4098
+ } else {
4099
+ initiator.removeEventListener('mousedown', initiatorMouseDownHandler);
4100
+ initiator.removeEventListener('mouseleave', initiatorMouseLeaveHandler);
4101
+ }
4291
4102
 
4292
- const isPositiveNumber = (x) => !Number.isNaN(+x) && +x >= 0;
4103
+ if (newType !== null) {
4104
+ setExtendLasso(newType);
4105
+ }
4106
+ };
4293
4107
 
4294
- const isStrictlyPositiveNumber = (x) => !Number.isNaN(+x) && +x > 0;
4108
+ const destroy = () => {
4109
+ initiatorParentElement.removeChild(initiator);
4110
+ longPressIndicatorParentElement.removeChild(longPress);
4111
+ window.removeEventListener('mouseup', mouseUpHandler);
4112
+ initiator.removeEventListener('click', initiatorClickHandler);
4113
+ initiator.removeEventListener('mousedown', initiatorMouseDownHandler);
4114
+ initiator.removeEventListener('mouseleave', initiatorMouseLeaveHandler);
4115
+ };
4295
4116
 
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;
4117
+ const withPublicMethods = () => (self) =>
4118
+ assign(self, {
4119
+ clear,
4120
+ destroy,
4121
+ end,
4122
+ extend: extendPublic,
4123
+ get,
4124
+ set,
4125
+ showInitiator,
4126
+ hideInitiator,
4127
+ showLongPressIndicator,
4128
+ hideLongPressIndicator,
4129
+ });
4304
4130
 
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
- });
4131
+ initiatorParentElement.appendChild(initiator);
4132
+ longPressIndicatorParentElement.appendChild(longPress);
4331
4133
 
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
- });
4134
+ set({
4135
+ onDraw,
4136
+ onStart,
4137
+ onEnd,
4138
+ enableInitiator,
4139
+ initiatorParentElement,
4140
+ type,
4141
+ brushSize,
4357
4142
  });
4358
4143
 
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
- ];
4370
-
4371
- const REGEX_IS_HEX = /(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i;
4144
+ return pipe(
4145
+ withStaticProperty('initiator', initiator),
4146
+ withStaticProperty('longPressIndicator', longPress),
4147
+ withPublicMethods(),
4148
+ withConstructor(createLasso),
4149
+ )({});
4150
+ };
4372
4151
 
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);
4152
+ const FRAGMENT_SHADER$1 = `
4153
+ precision highp float;
4379
4154
 
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;
4155
+ uniform float antiAliasing;
4386
4156
 
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);
4157
+ varying vec4 color;
4158
+ varying float finalPointSize;
4393
4159
 
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);
4160
+ float linearstep(float edge0, float edge1, float x) {
4161
+ return clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0);
4406
4162
  }
4407
4163
 
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;
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);
4417
4168
 
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];
4169
+ gl_FragColor = vec4(color.rgb, alpha * color.a);
4170
+ }
4171
+ `;
4423
4172
 
4424
- if (y1 <= py) {
4425
- if (y2 > py) {
4426
- const orientation = crossProduct(x1, y1, x2, y2, px, py);
4427
- if (orientation > 0) {
4428
- winding++;
4429
- }
4430
- }
4431
- } else if (y2 <= py) {
4432
- const orientation = crossProduct(x1, y1, x2, y2, px, py);
4433
- if (orientation < 0) {
4434
- winding--;
4435
- }
4436
- }
4173
+ const createVertexShader = (globalState) => `
4174
+ precision highp float;
4437
4175
 
4438
- j = i;
4439
- }
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;
4440
4208
 
4441
- return winding !== 0;
4442
- };
4209
+ attribute vec2 stateIndex;
4443
4210
 
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;
4211
+ varying vec4 color;
4212
+ varying float finalPointSize;
4450
4213
 
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;
4214
+ void main() {
4215
+ vec4 state = texture2D(stateTex, stateIndex);
4457
4216
 
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);
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
+ }
4464
4227
 
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));
4473
4228
 
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));
4229
+ // Determine color index
4230
+ float colorIndexZ = isColoredByZ * floor(state.z * colorMultiplicator);
4231
+ float colorIndexW = isColoredByW * floor(state.w * colorMultiplicator);
4482
4232
 
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]));
4233
+ // Multiply by the number of color states per color
4234
+ // I.e., normal, active, hover, background, etc.
4235
+ float colorIndex = (colorIndexZ + colorIndexW) * numColorStates;
4492
4236
 
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
- }
4237
+ // Half a "pixel" or "texel" in texture coordinates
4238
+ float colorLinearIndex = colorIndex + globalState;
4500
4239
 
4501
- if (a.length === 0) {
4502
- return true;
4503
- }
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);
4504
4244
 
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
- }
4245
+ vec2 colorTexIndex = vec2(
4246
+ (colorLinearIndex / colorTexRes) - colorRowIndex + colorTexEps,
4247
+ colorRowIndex / colorTexRes + colorTexEps
4248
+ );
4509
4249
 
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
- };
4250
+ color = texture2D(colorTex, colorTexIndex);
4515
4251
 
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);
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;
4525
4256
 
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);
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;
4535
4263
 
4536
- /**
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.
4543
- */
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
- }
4264
+ // Retrieve opacity
4265
+ ${
4266
+ (() => {
4267
+ // Drawing the inner border of selected points
4268
+ if (globalState === 3) return '';
4558
4269
 
4559
- if (isRgb(color)) {
4560
- const base = 255 ** !shouldNormalize;
4561
- const isNormalized = isNormFloatArray(color);
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;
4562
4276
 
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];
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);
4285
+ }
4286
+ `;
4287
+ })()
4573
4288
  }
4574
4289
 
4575
- if (isHex(color)) {
4576
- return hexToRgba(color, shouldNormalize);
4577
- }
4290
+ color.a = min(pointOpacityMax, color.a) * pointOpacityScale;
4291
+ finalPointSize = (pointSize * pointScale) + pointSizeExtra;
4292
+ gl_PointSize = finalPointSize;
4293
+ }
4294
+ `;
4578
4295
 
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];
4584
- };
4296
+ const FRAGMENT_SHADER = `precision highp float;
4585
4297
 
4586
- /**
4587
- * Flip the key-value pairs of an object
4588
- * @param {object} obj - Object to be flipped
4589
- * @return {object} Flipped object
4590
- */
4591
- const flipObj = (obj) =>
4592
- Object.entries(obj).reduce((out, [key, value]) => {
4593
- if (out[value]) {
4594
- out[value] = [...out[value], key];
4595
- } else {
4596
- out[value] = key;
4597
- }
4598
- return out;
4599
- }, {});
4298
+ varying vec4 color;
4600
4299
 
4601
- const rgbBrightness = (rgb) =>
4602
- 0.21 * rgb[0] + 0.72 * rgb[1] + 0.07 * rgb[2];
4300
+ void main() {
4301
+ gl_FragColor = color;
4302
+ }
4303
+ `;
4603
4304
 
4604
- /**
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
4610
- */
4611
- const clip = (value, minValue, maxValue) =>
4612
- Math.min(maxValue, Math.max(minValue, value));
4305
+ const SHADER$1 = `precision highp float;
4613
4306
 
4614
- /**
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
4618
- */
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;
4307
+ uniform sampler2D startStateTex;
4308
+ uniform sampler2D endStateTex;
4309
+ uniform float t;
4628
4310
 
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]);
4311
+ varying vec2 particleTextureIndex;
4312
+
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;
4638
4318
 
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]);
4319
+ // The category cannot be interpolated
4320
+ float endCategory = texture2D(endStateTex, particleTextureIndex).z;
4644
4321
 
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
- })();
4322
+ gl_FragColor = vec4(curr.xy, endCategory, curr.z);
4323
+ }`;
4662
4324
 
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
- });
4325
+ const SHADER = `precision highp float;
4702
4326
 
4703
- const isHorizontalLine = (annotation) =>
4704
- Number.isFinite(annotation.y) && !('x' in annotation);
4327
+ attribute vec2 position;
4328
+ varying vec2 particleTextureIndex;
4705
4329
 
4706
- const isVerticalLine = (annotation) =>
4707
- Number.isFinite(annotation.x) && !('y' in annotation);
4330
+ void main() {
4331
+ // map normalized device coords to texture coords
4332
+ particleTextureIndex = 0.5 * (1.0 + position);
4708
4333
 
4709
- const isDomRect = (annotation) =>
4710
- Number.isFinite(annotation.x) &&
4711
- Number.isFinite(annotation.y) &&
4712
- Number.isFinite(annotation.width) &&
4713
- Number.isFinite(annotation.height);
4334
+ gl_Position = vec4(position, 0, 1);
4335
+ }`;
4714
4336
 
4715
- const isRect = (annotation) =>
4716
- Number.isFinite(annotation.x1) &&
4717
- Number.isFinite(annotation.y1) &&
4718
- Number.isFinite(annotation.x2) &&
4719
- Number.isFinite(annotation.x2);
4337
+ /**
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
4342
+ */
4343
+ const checkReglExtensions = (regl, silent) => {
4344
+ if (!regl) {
4345
+ return false;
4346
+ }
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);
4359
+ };
4720
4360
 
4721
- const isPolygon = (annotation) =>
4722
- 'vertices' in annotation && annotation.vertices.length > 1;
4361
+ /**
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
4365
+ */
4366
+ const createRegl = (canvas) => {
4367
+ const gl = canvas.getContext('webgl', {
4368
+ antialias: true,
4369
+ preserveDrawingBuffer: true,
4370
+ });
4371
+ const extensions = [];
4723
4372
 
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--;
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);
4378
+ } else {
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
+ );
4734
4383
  }
4735
- array[j + 1] = current;
4736
4384
  }
4737
- return array;
4385
+
4386
+ return createOriginalRegl({ gl, extensions });
4738
4387
  };
4739
4388
 
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;
4389
+ /**
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
4396
+ */
4397
+ const dist = (x1, y1, x2, y2) =>
4398
+ Math.sqrt((x1 - x2) ** 2 + (y1 - y2) ** 2);
4748
4399
 
4749
- let isDestroyed = false;
4400
+ /**
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
4405
+ */
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;
4750
4411
 
4751
- if (!regl) {
4752
- regl = createRegl(canvas);
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;
4753
4417
  }
4754
4418
 
4755
- const isSupportingAllGlExtensions = checkReglExtensions(regl);
4756
-
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
- });
4419
+ return [xMin, yMin, xMax, yMax];
4420
+ };
4764
4421
 
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;
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;
4782
4434
 
4783
- vec3 approxLinearToSRGB (vec3 rgb, float gamma) {
4784
- return pow(clamp(rgb, vec3(0), vec3(1)), vec3(1.0 / gamma));
4785
- }
4435
+ const REGEX_HEX_TO_RGB = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
4786
4436
 
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
- });
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);
4813
4450
 
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
- };
4451
+ const isConditionalArray = (a, condition, { minLength = 0 } = {}) =>
4452
+ Array.isArray(a) && a.length >= minLength && a.every(condition);
4832
4453
 
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
- };
4454
+ const isPositiveNumber = (x) => !Number.isNaN(+x) && +x >= 0;
4850
4455
 
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
- };
4456
+ const isStrictlyPositiveNumber = (x) => !Number.isNaN(+x) && +x > 0;
4860
4457
 
4861
- const drawFns = new Set();
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;
4862
4466
 
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);
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';
4482
+ }
4483
+ image.src = src;
4484
+ image.onload = () => {
4485
+ resolve(image);
4871
4486
  };
4872
- };
4487
+ const rejectPromise = () => {
4488
+ reject(new Error(IMAGE_LOAD_ERROR));
4489
+ };
4490
+ image.onerror = rejectPromise;
4491
+ setTimeout(rejectPromise, timeout);
4492
+ });
4873
4493
 
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
- }
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
+ });
4881
4519
  });
4882
4520
 
4883
- const resize = (
4884
- /** @type {number} */ customWidth,
4885
- /** @type {number} */ customHeight,
4886
- ) => {
4887
- const width = customWidth == null ? window.innerWidth : customWidth;
4888
- const height = customHeight == null ? window.innerHeight : customHeight;
4889
- canvas.width = width * window.devicePixelRatio;
4890
- canvas.height = height * window.devicePixelRatio;
4891
- fboRes[0] = canvas.width;
4892
- fboRes[1] = canvas.height;
4893
- fbo.resize(...fboRes);
4894
- };
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
+ ];
4895
4532
 
4896
- const resizeHandler = () => {
4897
- resize();
4898
- };
4533
+ const REGEX_IS_HEX = /(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i;
4899
4534
 
4900
- if (!options.canvas) {
4901
- window.addEventListener('resize', resizeHandler);
4902
- window.addEventListener('orientationchange', resizeHandler);
4903
- resize();
4904
- }
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);
4905
4541
 
4906
- /**
4907
- * Destroy the renderer to free resources and cancel animation frames
4908
- */
4909
- const destroy = () => {
4910
- isDestroyed = true;
4911
- window.removeEventListener('resize', resizeHandler);
4912
- window.removeEventListener('orientationchange', resizeHandler);
4913
- frame.cancel();
4914
- canvas = undefined;
4915
- regl.destroy();
4916
- regl = undefined;
4917
- };
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;
4918
4548
 
4919
- return {
4920
- /**
4921
- * Get the associated canvas element
4922
- * @return {HTMLCanvasElement} The associated canvas element
4923
- */
4924
- get canvas() {
4925
- return canvas;
4926
- },
4927
- /**
4928
- * Get the associated Regl instance
4929
- * @return {import('regl').Regl} The associated Regl instance
4930
- */
4931
- get regl() {
4932
- return regl;
4933
- },
4934
- /**
4935
- * Get the gamma value
4936
- * @return {number} The gamma value
4937
- */
4938
- get gamma() {
4939
- return gamma;
4940
- },
4941
- /**
4942
- * Set gamma to a new value
4943
- * @param {number} newGamma - The new gamma value
4944
- */
4945
- set gamma(newGamma) {
4946
- gamma = +newGamma;
4947
- },
4948
- /**
4949
- * Get whether the browser supports all necessary WebGL features
4950
- * @return {boolean} If `true` the browser supports all necessary WebGL features
4951
- */
4952
- get isSupported() {
4953
- return isSupportingAllGlExtensions;
4954
- },
4955
- /**
4956
- * Get whether the renderer (and its Regl instance) is destroyed
4957
- * @return {boolean} If `true` the renderer is destroyed
4958
- */
4959
- get isDestroyed() {
4960
- return isDestroyed;
4961
- },
4962
- render,
4963
- resize,
4964
- onFrame,
4965
- refresh,
4966
- destroy,
4967
- };
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);
4555
+
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
+ }
4569
+
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;
4579
+
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];
4585
+
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
+ }
4599
+
4600
+ j = i;
4601
+ }
4602
+
4603
+ return winding !== 0;
4968
4604
  };
4969
4605
 
4970
- const FRAGMENT_SHADER$2 = `
4971
- precision mediump float;
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;
4972
4612
 
4973
- uniform sampler2D texture;
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;
4974
4619
 
4975
- varying vec2 uv;
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);
4976
4626
 
4977
- void main () {
4978
- gl_FragColor = texture2D(texture, uv);
4979
- }
4980
- `;
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));
4981
4635
 
4982
- const VERTEX_SHADER = `
4983
- precision mediump float;
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));
4984
4644
 
4985
- uniform mat4 modelViewProjection;
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]));
4986
4654
 
4987
- attribute vec2 position;
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
+ }
4988
4662
 
4989
- varying vec2 uv;
4663
+ if (a.length === 0) {
4664
+ return true;
4665
+ }
4990
4666
 
4991
- void main () {
4992
- uv = position;
4993
- gl_Position = modelViewProjection * vec4(-1.0 + 2.0 * uv.x, 1.0 - 2.0 * uv.y, 0, 1);
4994
- }
4995
- `;
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
+ }
4996
4671
 
4997
- const FRAGMENT_SHADER$1 = `precision highp float;
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
+ };
4998
4677
 
4999
- varying vec4 color;
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);
5000
4687
 
5001
- void main() {
5002
- gl_FragColor = color;
5003
- }
5004
- `;
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);
5005
4697
 
5006
- const SHADER$1 = `precision highp float;
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
+ }
5007
4720
 
5008
- uniform sampler2D startStateTex;
5009
- uniform sampler2D endStateTex;
5010
- uniform float t;
4721
+ if (isRgb(color)) {
4722
+ const base = 255 ** !shouldNormalize;
4723
+ const isNormalized = isNormFloatArray(color);
5011
4724
 
5012
- varying vec2 particleTextureIndex;
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
+ }
5013
4736
 
5014
- void main() {
5015
- // Interpolate x, y, and value
5016
- vec3 start = texture2D(startStateTex, particleTextureIndex).xyw;
5017
- vec3 end = texture2D(endStateTex, particleTextureIndex).xyw;
5018
- vec3 curr = start * (1.0 - t) + end * t;
4737
+ if (isHex(color)) {
4738
+ return hexToRgba(color, shouldNormalize);
4739
+ }
5019
4740
 
5020
- // The category cannot be interpolated
5021
- float endCategory = texture2D(endStateTex, particleTextureIndex).z;
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
+ };
5022
4747
 
5023
- gl_FragColor = vec4(curr.xy, endCategory, curr.z);
5024
- }`;
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
+ }, {});
5025
4762
 
5026
- const SHADER = `precision highp float;
4763
+ const rgbBrightness = (rgb) =>
4764
+ 0.21 * rgb[0] + 0.72 * rgb[1] + 0.07 * rgb[2];
4765
+
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));
4775
+
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;
4790
+
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]);
4800
+
4801
+ const getLO =
4802
+ (Array.isArray(points.lineOrder) ||
4803
+ ArrayBuffer.isView(points.lineOrder)) &&
4804
+ ((i) => points.lineOrder[i]);
5027
4805
 
5028
- attribute vec2 position;
5029
- varying vec2 particleTextureIndex;
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
+ })();
5030
4823
 
5031
- void main() {
5032
- // map normalized device coords to texture coords
5033
- particleTextureIndex = 0.5 * (1.0 + position);
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
+ });
5034
4863
 
5035
- gl_Position = vec4(position, 0, 1);
5036
- }`;
4864
+ const isHorizontalLine = (annotation) =>
4865
+ Number.isFinite(annotation.y) && !('x' in annotation);
5037
4866
 
5038
- const FRAGMENT_SHADER = `
5039
- precision highp float;
4867
+ const isVerticalLine = (annotation) =>
4868
+ Number.isFinite(annotation.x) && !('y' in annotation);
5040
4869
 
5041
- uniform float antiAliasing;
4870
+ const isDomRect = (annotation) =>
4871
+ Number.isFinite(annotation.x) &&
4872
+ Number.isFinite(annotation.y) &&
4873
+ Number.isFinite(annotation.width) &&
4874
+ Number.isFinite(annotation.height);
5042
4875
 
5043
- varying vec4 color;
5044
- varying float finalPointSize;
4876
+ const isRect = (annotation) =>
4877
+ Number.isFinite(annotation.x1) &&
4878
+ Number.isFinite(annotation.y1) &&
4879
+ Number.isFinite(annotation.x2) &&
4880
+ Number.isFinite(annotation.x2);
5045
4881
 
5046
- float linearstep(float edge0, float edge1, float x) {
5047
- return clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0);
5048
- }
4882
+ const isPolygonAnnotation = (annotation) =>
4883
+ 'vertices' in annotation && annotation.vertices.length > 1;
5049
4884
 
5050
- void main() {
5051
- vec2 c = gl_PointCoord * 2.0 - 1.0;
5052
- float sdf = length(c) * finalPointSize;
5053
- float alpha = linearstep(finalPointSize + antiAliasing, finalPointSize - antiAliasing, sdf);
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
+ }
5054
4894
 
5055
- gl_FragColor = vec4(color.rgb, alpha * color.a);
5056
- }
5057
- `;
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
+ }
5058
4905
 
5059
- const createVertexShader = (globalState) => `
5060
- precision highp float;
4906
+ return true;
4907
+ };
5061
4908
 
5062
- uniform sampler2D colorTex;
5063
- uniform float colorTexRes;
5064
- uniform float colorTexEps;
5065
- uniform sampler2D stateTex;
5066
- uniform float stateTexRes;
5067
- uniform float stateTexEps;
5068
- uniform float devicePixelRatio;
5069
- uniform sampler2D encodingTex;
5070
- uniform float encodingTexRes;
5071
- uniform float encodingTexEps;
5072
- uniform float pointSizeExtra;
5073
- uniform float pointOpacityMax;
5074
- uniform float pointOpacityScale;
5075
- uniform float numPoints;
5076
- uniform float globalState;
5077
- uniform float isColoredByZ;
5078
- uniform float isColoredByW;
5079
- uniform float isOpacityByZ;
5080
- uniform float isOpacityByW;
5081
- uniform float isOpacityByDensity;
5082
- uniform float isSizedByZ;
5083
- uniform float isSizedByW;
5084
- uniform float isPixelAligned;
5085
- uniform float colorMultiplicator;
5086
- uniform float opacityMultiplicator;
5087
- uniform float opacityDensity;
5088
- uniform float sizeMultiplicator;
5089
- uniform float numColorStates;
5090
- uniform float pointScale;
5091
- uniform float drawingBufferWidth;
5092
- uniform float drawingBufferHeight;
5093
- uniform mat4 modelViewProjection;
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
+ };
5094
4923
 
5095
- attribute vec2 stateIndex;
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
+ };
5096
4939
 
5097
- varying vec4 color;
5098
- varying float finalPointSize;
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;
5099
4948
 
5100
- void main() {
5101
- vec4 state = texture2D(stateTex, stateIndex);
4949
+ let isDestroyed = false;
5102
4950
 
5103
- if (isPixelAligned < 0.5) {
5104
- gl_Position = modelViewProjection * vec4(state.x, state.y, 0.0, 1.0);
5105
- } else {
5106
- vec4 clipSpacePosition = modelViewProjection * vec4(state.x, state.y, 0.0, 1.0);
5107
- vec2 ndcPosition = clipSpacePosition.xy / clipSpacePosition.w;
5108
- vec2 pixelPos = 0.5 * (ndcPosition + 1.0) * vec2(drawingBufferWidth, drawingBufferHeight);
5109
- pixelPos = floor(pixelPos + 0.5); // Snap to nearest pixel
5110
- vec2 snappedPosition = (pixelPos / vec2(drawingBufferWidth, drawingBufferHeight)) * 2.0 - 1.0;
5111
- gl_Position = vec4(snappedPosition, 0.0, 1.0);
4951
+ if (!regl) {
4952
+ regl = createRegl(canvas);
5112
4953
  }
5113
4954
 
4955
+ const isSupportingAllGlExtensions = checkReglExtensions(regl);
5114
4956
 
5115
- // Determine color index
5116
- float colorIndexZ = isColoredByZ * floor(state.z * colorMultiplicator);
5117
- float colorIndexW = isColoredByW * floor(state.w * colorMultiplicator);
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
+ });
5118
4964
 
5119
- // Multiply by the number of color states per color
5120
- // I.e., normal, active, hover, background, etc.
5121
- float colorIndex = (colorIndexZ + colorIndexW) * numColorStates;
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;
5122
4982
 
5123
- // Half a "pixel" or "texel" in texture coordinates
5124
- float colorLinearIndex = colorIndex + globalState;
4983
+ vec3 approxLinearToSRGB (vec3 rgb, float gamma) {
4984
+ return pow(clamp(rgb, vec3(0), vec3(1)), vec3(1.0 / gamma));
4985
+ }
4986
+
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
+ });
5011
+
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
+ };
5125
5030
 
5126
- // Need to add cEps here to avoid floating point issue that can lead to
5127
- // dramatic changes in which color is loaded as floor(3/2.9999) = 1 but
5128
- // floor(3/3.0001) = 0!
5129
- 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
+ };
5130
5048
 
5131
- vec2 colorTexIndex = vec2(
5132
- (colorLinearIndex / colorTexRes) - colorRowIndex + colorTexEps,
5133
- colorRowIndex / colorTexRes + colorTexEps
5134
- );
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
+ };
5135
5058
 
5136
- color = texture2D(colorTex, colorTexIndex);
5059
+ const drawFns = new Set();
5137
5060
 
5138
- // Retrieve point size
5139
- float pointSizeIndexZ = isSizedByZ * floor(state.z * sizeMultiplicator);
5140
- float pointSizeIndexW = isSizedByW * floor(state.w * sizeMultiplicator);
5141
- 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
+ };
5142
5071
 
5143
- float pointSizeRowIndex = floor((pointSizeIndex + encodingTexEps) / encodingTexRes);
5144
- vec2 pointSizeTexIndex = vec2(
5145
- (pointSizeIndex / encodingTexRes) - pointSizeRowIndex + encodingTexEps,
5146
- pointSizeRowIndex / encodingTexRes + encodingTexEps
5147
- );
5148
- 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
+ });
5149
5080
 
5150
- // Retrieve opacity
5151
- ${
5152
- (() => {
5153
- // Drawing the inner border of selected points
5154
- 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
+ };
5155
5093
 
5156
- // Draw points with opacity encoding or dynamic opacity
5157
- return `
5158
- if (isOpacityByDensity < 0.5) {
5159
- float opacityIndexZ = isOpacityByZ * floor(state.z * opacityMultiplicator);
5160
- float opacityIndexW = isOpacityByW * floor(state.w * opacityMultiplicator);
5161
- float opacityIndex = opacityIndexZ + opacityIndexW;
5094
+ const resizeHandler = () => {
5095
+ resize();
5096
+ };
5162
5097
 
5163
- float opacityRowIndex = floor((opacityIndex + encodingTexEps) / encodingTexRes);
5164
- vec2 opacityTexIndex = vec2(
5165
- (opacityIndex / encodingTexRes) - opacityRowIndex + encodingTexEps,
5166
- opacityRowIndex / encodingTexRes + encodingTexEps
5167
- );
5168
- color.a = texture2D(encodingTex, opacityTexIndex)[${1 + globalState}];
5169
- } else {
5170
- color.a = min(1.0, opacityDensity + globalState);
5171
- }
5172
- `;
5173
- })()
5098
+ if (!options.canvas) {
5099
+ window.addEventListener('resize', resizeHandler);
5100
+ window.addEventListener('orientationchange', resizeHandler);
5101
+ resize();
5174
5102
  }
5175
5103
 
5176
- color.a = min(pointOpacityMax, color.a) * pointOpacityScale;
5177
- finalPointSize = (pointSize * pointScale) + pointSizeExtra;
5178
- gl_PointSize = finalPointSize;
5179
- }
5180
- `;
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
+ };
5181
5167
 
5182
5168
  /* eslint-env worker */
5183
5169
  /* eslint no-restricted-globals: 1 */
@@ -5286,7 +5272,6 @@ const worker = function worker() {
5286
5272
  * @param {[type]} simplified [description]
5287
5273
  * @return {[type]} [description]
5288
5274
  */
5289
- // biome-ignore lint/style/useNamingConvention: DP stands for Douglas Peucker
5290
5275
  const simplifyDPStep = (points, first, last, tolerance, simplified) => {
5291
5276
  let maxDist = tolerance;
5292
5277
  let index;
@@ -5394,7 +5379,6 @@ const worker = function worker() {
5394
5379
  const groupedPoints = {};
5395
5380
 
5396
5381
  const isOrdered = !Number.isNaN(+points[0][5]);
5397
- // biome-ignore lint/complexity/noForEach: somehow for .. of does not work in a worker
5398
5382
  points.forEach((point) => {
5399
5383
  const segId = point[4];
5400
5384
 
@@ -5410,7 +5394,6 @@ const worker = function worker() {
5410
5394
  });
5411
5395
 
5412
5396
  // The filtering ensures that non-existing array entries are removed
5413
- // biome-ignore lint/complexity/noForEach: somehow for .. of does not work in a worker
5414
5397
  Object.entries(groupedPoints).forEach((idPoints) => {
5415
5398
  groupedPoints[idPoints[0]] = idPoints[1].filter((v) => v);
5416
5399
  // Store the first point as the reference
@@ -5467,8 +5450,6 @@ const createSplineCurve = (
5467
5450
  worker$1.postMessage({ points, options });
5468
5451
  });
5469
5452
 
5470
- var version = "1.14.1-hemato.3";
5471
-
5472
5453
  const deprecations = {
5473
5454
  showRecticle: {
5474
5455
  replacement: 'showReticle',
@@ -5577,8 +5558,8 @@ const createScatterplot = (
5577
5558
  deselectOnEscape = DEFAULT_DESELECT_ON_ESCAPE,
5578
5559
  lassoColor = DEFAULT_LASSO_COLOR,
5579
5560
  lassoLineWidth = DEFAULT_LASSO_LINE_WIDTH,
5580
- lassoMinDelay = DEFAULT_LASSO_MIN_DELAY,
5581
- lassoMinDist = DEFAULT_LASSO_MIN_DIST,
5561
+ lassoMinDelay = DEFAULT_LASSO_MIN_DELAY$1,
5562
+ lassoMinDist = DEFAULT_LASSO_MIN_DIST$1,
5582
5563
  lassoMode = DEFAULT_LASSO_MODE,
5583
5564
  lassoClearEvent = DEFAULT_LASSO_CLEAR_EVENT,
5584
5565
  lassoInitiator = DEFAULT_LASSO_INITIATOR,
@@ -5589,7 +5570,7 @@ const createScatterplot = (
5589
5570
  lassoLongPressAfterEffectTime = DEFAULT_LASSO_LONG_PRESS_AFTER_EFFECT_TIME,
5590
5571
  lassoLongPressEffectDelay = DEFAULT_LASSO_LONG_PRESS_EFFECT_DELAY,
5591
5572
  lassoLongPressRevertEffectTime = DEFAULT_LASSO_LONG_PRESS_REVERT_EFFECT_TIME,
5592
- lassoType = DEFAULT_LASSO_TYPE,
5573
+ lassoType = DEFAULT_LASSO_TYPE$1,
5593
5574
  lassoBrushSize = DEFAULT_LASSO_BRUSH_SIZE,
5594
5575
  actionKeyMap = DEFAULT_ACTION_KEY_MAP,
5595
5576
  mouseMode = DEFAULT_MOUSE_MODE,
@@ -5621,6 +5602,7 @@ const createScatterplot = (
5621
5602
  opacityInactiveMax = DEFAULT_OPACITY_INACTIVE_MAX,
5622
5603
  opacityInactiveScale = DEFAULT_OPACITY_INACTIVE_SCALE,
5623
5604
  sizeBy = DEFAULT_SIZE_BY,
5605
+ pointOrder = DEFAULT_POINT_ORDER,
5624
5606
  pointScaleMode = DEFAULT_POINT_SCALE_MODE,
5625
5607
  height = DEFAULT_HEIGHT,
5626
5608
  width = DEFAULT_WIDTH,
@@ -5696,9 +5678,7 @@ const createScatterplot = (
5696
5678
  let pointConnections;
5697
5679
  let pointConnectionMap;
5698
5680
  let computingPointConnectionCurves;
5699
- // biome-ignore lint/style/useNamingConvention: HLine stands for HorizontalLine
5700
5681
  let reticleHLine;
5701
- // biome-ignore lint/style/useNamingConvention: VLine stands for VerticalLine
5702
5682
  let reticleVLine;
5703
5683
  let computedPointSizeMouseDetection;
5704
5684
  let lassoInitiatorTimeout;
@@ -5831,6 +5811,8 @@ const createScatterplot = (
5831
5811
  let normalPointsIndexBuffer; // Buffer holding the indices pointing to the correct texel
5832
5812
  let selectedPointsIndexBuffer; // Used for pointing to the selected texels
5833
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
5834
5816
 
5835
5817
  let cameraZoomTargetStart; // Stores the start (i.e., current) camera target for zooming
5836
5818
  let cameraZoomTargetEnd; // Stores the end camera target for zooming
@@ -5853,9 +5835,7 @@ const createScatterplot = (
5853
5835
  let isAnnotationsDrawn = false;
5854
5836
  let isMouseOverCanvasChecked = false;
5855
5837
 
5856
- // biome-ignore lint/style/useNamingConvention: ZDate is not one word
5857
5838
  let valueZDataType = CATEGORICAL;
5858
- // biome-ignore lint/style/useNamingConvention: WDate is not one word
5859
5839
  let valueWDataType = CATEGORICAL;
5860
5840
 
5861
5841
  /** @type{number|undefined} */
@@ -5929,9 +5909,7 @@ const createScatterplot = (
5929
5909
  return points;
5930
5910
  };
5931
5911
 
5932
- // biome-ignore lint/style/useNamingConvention: BBox stands for BoundingBox
5933
5912
  const getPointsInBBox = (x0, y0, x1, y1) => {
5934
- // biome-ignore lint/style/useNamingConvention: BBox stands for BoundingBox
5935
5913
  const pointsInBBox = spatialIndex.range(x0, y0, x1, y1);
5936
5914
  if (isPointsFiltered) {
5937
5915
  return pointsInBBox.filter((i) => filteredPointsSet.has(i));
@@ -5946,7 +5924,6 @@ const createScatterplot = (
5946
5924
  const pointSizeNdc = getPointSizeNdc(4);
5947
5925
 
5948
5926
  // Get all points within a close range
5949
- // biome-ignore lint/style/useNamingConvention: BBox stands for BoundingBox
5950
5927
  const pointsInBBox = getPointsInBBox(
5951
5928
  xNdc - pointSizeNdc,
5952
5929
  yNdc - pointSizeNdc,
@@ -5984,7 +5961,6 @@ const createScatterplot = (
5984
5961
  }
5985
5962
 
5986
5963
  // ...to efficiently preselect potentially selected points
5987
- // biome-ignore lint/style/useNamingConvention: BBox stands for BoundingBox
5988
5964
  const pointsInBBox = getPointsInBBox(...bBox);
5989
5965
  // next we test each point in the bounding box if it is in the polygon too
5990
5966
  const pointsInPolygon = [];
@@ -6089,6 +6065,40 @@ const createScatterplot = (
6089
6065
  }
6090
6066
  };
6091
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
+
6092
6102
  /**
6093
6103
  * Select and highlight a set of points
6094
6104
  * @param {number | number[]} pointIdxs
@@ -6192,6 +6202,47 @@ const createScatterplot = (
6192
6202
  draw = true;
6193
6203
  };
6194
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
+
6195
6246
  /**
6196
6247
  * @param {number} point
6197
6248
  * @param {import('./types').ScatterplotMethodOptions['hover']} options
@@ -6868,6 +6919,44 @@ const createScatterplot = (
6868
6919
  const setSizeBy = (type) => {
6869
6920
  sizeBy = getEncodingType(type, DEFAULT_SIZE_BY);
6870
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
+ };
6871
6960
  const setPointConnectionColorBy = (type) => {
6872
6961
  pointConnectionColorBy = getEncodingType(
6873
6962
  type,
@@ -7035,16 +7124,14 @@ const createScatterplot = (
7035
7124
  getPointOpacityScale = getPointOpacityScaleBase,
7036
7125
  ) =>
7037
7126
  renderer.regl({
7038
- frag: renderPointsAsSquares ? FRAGMENT_SHADER$1 : FRAGMENT_SHADER,
7127
+ frag: renderPointsAsSquares ? FRAGMENT_SHADER : FRAGMENT_SHADER$1,
7039
7128
  vert: createVertexShader(globalState),
7040
7129
 
7041
7130
  blend: {
7042
7131
  enable: !disableAlphaBlending,
7043
7132
  func: {
7044
- // biome-ignore lint/style/useNamingConvention: Regl specific
7045
7133
  srcRGB: 'src alpha',
7046
7134
  srcAlpha: 'one',
7047
- // biome-ignore lint/style/useNamingConvention: Regl specific
7048
7135
  dstRGB: 'one minus src alpha',
7049
7136
  dstAlpha: 'one minus src alpha',
7050
7137
  },
@@ -7185,10 +7272,8 @@ const createScatterplot = (
7185
7272
  blend: {
7186
7273
  enable: true,
7187
7274
  func: {
7188
- // biome-ignore lint/style/useNamingConvention: Regl specific
7189
7275
  srcRGB: 'src alpha',
7190
7276
  srcAlpha: 'one',
7191
- // biome-ignore lint/style/useNamingConvention: Regl specific
7192
7277
  dstRGB: 'one minus src alpha',
7193
7278
  dstAlpha: 'one minus src alpha',
7194
7279
  },
@@ -7266,6 +7351,51 @@ const createScatterplot = (
7266
7351
  return index;
7267
7352
  };
7268
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
+
7269
7399
  const createStateTexture = (newPoints, dataTypes = {}) => {
7270
7400
  const numNewPoints = newPoints.length;
7271
7401
  stateTexRes = Math.max(2, Math.ceil(Math.sqrt(numNewPoints)));
@@ -7357,9 +7487,18 @@ const createScatterplot = (
7357
7487
  const preventFilterReset =
7358
7488
  options?.preventFilterReset && newPoints.length === numPoints;
7359
7489
 
7490
+ const prevNumPoints = numPoints;
7360
7491
  numPoints = newPoints.length;
7361
7492
  numPointsInView = numPoints;
7362
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
+
7363
7502
  if (stateTex) {
7364
7503
  stateTex.destroy();
7365
7504
  }
@@ -7369,10 +7508,11 @@ const createScatterplot = (
7369
7508
  });
7370
7509
 
7371
7510
  if (!preventFilterReset) {
7511
+ computePointOrderIndex();
7372
7512
  normalPointsIndexBuffer({
7373
7513
  usage: 'static',
7374
7514
  type: 'float',
7375
- data: createPointIndex(numPoints),
7515
+ data: getEffectivePointIndex(numPoints),
7376
7516
  });
7377
7517
  }
7378
7518
 
@@ -7613,7 +7753,7 @@ const createScatterplot = (
7613
7753
  const unfilter = ({ preventEvent = false } = {}) => {
7614
7754
  isPointsFiltered = false;
7615
7755
  filteredPointsSet.clear();
7616
- normalPointsIndexBuffer.subdata(createPointIndex(numPoints));
7756
+ normalPointsIndexBuffer.subdata(getEffectivePointIndex(numPoints));
7617
7757
 
7618
7758
  return new Promise((resolve) => {
7619
7759
  const finish = () => {
@@ -7672,9 +7812,20 @@ const createScatterplot = (
7672
7812
  }
7673
7813
  }
7674
7814
 
7675
- 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
+ }
7676
7827
 
7677
- for (const pointIdx of sortedFilteredPoints) {
7828
+ for (const pointIdx of orderedFilteredPoints) {
7678
7829
  filteredPointsBuffer.push.apply(
7679
7830
  filteredPointsBuffer,
7680
7831
  indexToStateTexCoord(pointIdx),
@@ -8061,7 +8212,7 @@ const createScatterplot = (
8061
8212
  continue;
8062
8213
  }
8063
8214
 
8064
- if (isPolygon(annotation)) {
8215
+ if (isPolygonAnnotation(annotation)) {
8065
8216
  newPoints.push(annotation.vertices.flatMap(identity));
8066
8217
  addColorAndWidth(annotation);
8067
8218
  }
@@ -8102,7 +8253,6 @@ const createScatterplot = (
8102
8253
  * @param {number[]} pointIdxs - A list of point indices
8103
8254
  * @returns {import('./types').Rect} The bounding box
8104
8255
  */
8105
- // biome-ignore lint/style/useNamingConvention: BBox stands for BoundingBox
8106
8256
  const getBBoxOfPoints = (pointIdxs) => {
8107
8257
  let xMin = Number.POSITIVE_INFINITY;
8108
8258
  let xMax = Number.NEGATIVE_INFINITY;
@@ -8138,7 +8288,6 @@ const createScatterplot = (
8138
8288
  // Vertical field of view
8139
8289
  // The Arc Tangent is based on the original camera position. Otherwise
8140
8290
  // we would have to do `Math.atan(1 / camera.view[5])`
8141
- // biome-ignore lint/style/useNamingConvention: FOV stands for field of view
8142
8291
  const vFOV = 2 * Math.atan(1);
8143
8292
 
8144
8293
  const aspectRatio = viewAspectRatio / dataAspectRatio;
@@ -8538,7 +8687,6 @@ const createScatterplot = (
8538
8687
  reticleVLine.setStyle({ color: reticleColor });
8539
8688
  };
8540
8689
 
8541
- // biome-ignore lint/style/useNamingConvention: XScale are two words
8542
8690
  const setXScale = (newXScale) => {
8543
8691
  if (!newXScale) {
8544
8692
  return;
@@ -8551,7 +8699,6 @@ const createScatterplot = (
8551
8699
  updateScales();
8552
8700
  };
8553
8701
 
8554
- // biome-ignore lint/style/useNamingConvention: YScale are two words
8555
8702
  const setYScale = (newYScale) => {
8556
8703
  if (!newYScale) {
8557
8704
  return;
@@ -8725,7 +8872,6 @@ const createScatterplot = (
8725
8872
  annotationLineWidth = +newAnnotationLineWidth;
8726
8873
  };
8727
8874
 
8728
- // biome-ignore lint/style/useNamingConvention: HVLine stands for horizontal vertical line
8729
8875
  const setAnnotationHVLineLimit = (newAnnotationHVLineLimit) => {
8730
8876
  annotationHVLineLimit = +newAnnotationHVLineLimit;
8731
8877
  };
@@ -8802,6 +8948,10 @@ const createScatterplot = (
8802
8948
  return sizeBy;
8803
8949
  }
8804
8950
 
8951
+ if (property === 'pointOrder') {
8952
+ return pointOrder !== null ? [...pointOrder] : null;
8953
+ }
8954
+
8805
8955
  if (property === 'deselectOnDblClick') {
8806
8956
  return deselectOnDblClick;
8807
8957
  }
@@ -9188,6 +9338,10 @@ const createScatterplot = (
9188
9338
  setSizeBy(properties.sizeBy);
9189
9339
  }
9190
9340
 
9341
+ if (properties.pointOrder !== undefined) {
9342
+ setPointOrder(properties.pointOrder);
9343
+ }
9344
+
9191
9345
  if (properties.opacity !== undefined) {
9192
9346
  setOpacity(properties.opacity);
9193
9347
  }
@@ -9885,6 +10039,9 @@ const createScatterplot = (
9885
10039
  init();
9886
10040
 
9887
10041
  return {
10042
+ attachSpatialIndex: (buffer) => {
10043
+ spatialIndex = kdbushFrom(buffer);
10044
+ },
9888
10045
  /**
9889
10046
  * Get whether the browser supports all necessary WebGL features
9890
10047
  * @return {boolean} If `true` the browser supports all necessary WebGL features
@@ -9908,6 +10065,7 @@ const createScatterplot = (
9908
10065
  get,
9909
10066
  getScreenPosition,
9910
10067
  hover,
10068
+ lassoSelect,
9911
10069
  redraw,
9912
10070
  refresh: renderer.refresh,
9913
10071
  reset: withDraw(reset),