@hema-to/regl-scatterplot 1.14.1 → 1.16.0-hemato.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +94 -0
- package/dist/regl-scatterplot.esm.d.ts +2 -0
- package/dist/regl-scatterplot.esm.js +1678 -1538
- package/dist/regl-scatterplot.js +1747 -1756
- package/dist/regl-scatterplot.min.js +3 -8
- package/dist/types.d.ts +6 -7
- package/package.json +27 -27
- package/src/constants.js +1 -0
- package/src/index.js +217 -43
- package/src/kdbush-worker.js +0 -1
- package/src/kdbush.js +2 -3
- package/src/lasso-manager/index.js +6 -8
- package/src/renderer.js +2 -22
- package/src/spline-curve-worker.js +0 -3
- package/src/types.d.ts +6 -7
- package/src/utils.js +40 -4
package/dist/types.d.ts
CHANGED
|
@@ -10,14 +10,13 @@ type Value = 'value' | 'value2' | 'valueB' | 'valueW' | 'w';
|
|
|
10
10
|
type DataEncoding = Category | Value;
|
|
11
11
|
type PointDataEncoding = DataEncoding | 'inherit' | 'segment';
|
|
12
12
|
|
|
13
|
-
type KeyAction = 'lasso' | 'rotate' | 'merge';
|
|
13
|
+
type KeyAction = 'lasso' | 'rotate' | 'merge' | 'intersect' | 'remove';
|
|
14
14
|
type KeyMap = Record<'alt' | 'cmd' | 'ctrl' | 'meta' | 'shift', KeyAction>;
|
|
15
15
|
|
|
16
16
|
type MouseMode = 'panZoom' | 'lasso' | 'rotate';
|
|
17
17
|
|
|
18
18
|
type PointScaleMode = 'constant' | 'asinh' | 'linear';
|
|
19
19
|
|
|
20
|
-
// biome-ignore lint/style/useNamingConvention: ZWData are three words, z, w, and data
|
|
21
20
|
type ZWDataType = 'continuous' | 'categorical';
|
|
22
21
|
|
|
23
22
|
// biome-ignore lint/suspicious/noExplicitAny: Untyped external library
|
|
@@ -96,14 +95,12 @@ interface BaseAnnotation {
|
|
|
96
95
|
lineWidth?: number;
|
|
97
96
|
}
|
|
98
97
|
|
|
99
|
-
// biome-ignore lint/style/useNamingConvention: HLine stands for HorizontalLine
|
|
100
98
|
interface AnnotationHLine extends BaseAnnotation {
|
|
101
99
|
y: number;
|
|
102
100
|
x1?: number;
|
|
103
101
|
x2?: number;
|
|
104
102
|
}
|
|
105
103
|
|
|
106
|
-
// biome-ignore lint/style/useNamingConvention: HLine stands for VerticalLine
|
|
107
104
|
interface AnnotationVLine extends BaseAnnotation {
|
|
108
105
|
x: number;
|
|
109
106
|
y1?: number;
|
|
@@ -156,13 +153,13 @@ interface BaseOptions {
|
|
|
156
153
|
aspectRatio: number;
|
|
157
154
|
annotationLineColor: Color;
|
|
158
155
|
annotationLineWidth: number;
|
|
159
|
-
// biome-ignore lint/style/useNamingConvention: HVLine stands for HorizontalVerticalLine
|
|
160
156
|
annotationHVLineLimit: number;
|
|
161
157
|
// Nullifiable
|
|
162
158
|
backgroundImage: null | import('regl').Texture2D | string;
|
|
163
159
|
colorBy: null | DataEncoding;
|
|
164
160
|
sizeBy: null | DataEncoding;
|
|
165
161
|
opacityBy: null | DataEncoding;
|
|
162
|
+
pointOrder: null | number[] | Int32Array | Uint32Array;
|
|
166
163
|
xScale: null | Scale;
|
|
167
164
|
yScale: null | Scale;
|
|
168
165
|
pointScaleMode: PointScaleMode;
|
|
@@ -171,7 +168,6 @@ interface BaseOptions {
|
|
|
171
168
|
pixelAligned: boolean;
|
|
172
169
|
}
|
|
173
170
|
|
|
174
|
-
// biome-ignore lint/style/useNamingConvention: KDBush is a library name
|
|
175
171
|
export interface CreateKDBushOptions {
|
|
176
172
|
node: number;
|
|
177
173
|
useWorker: boolean;
|
|
@@ -295,7 +291,10 @@ export type Events = import('pub-sub-es').Event<
|
|
|
295
291
|
{ coordinates: number[] }
|
|
296
292
|
> &
|
|
297
293
|
import('pub-sub-es').Event<'pointOver' | 'pointOut', number> &
|
|
298
|
-
import('pub-sub-es').Event<
|
|
294
|
+
import('pub-sub-es').Event<
|
|
295
|
+
'select' | 'focus',
|
|
296
|
+
{ points: number[]; lassoMode: LassoOptions['mode'] }
|
|
297
|
+
> &
|
|
299
298
|
import('pub-sub-es').Event<'points', { points: number[][] }> &
|
|
300
299
|
import('pub-sub-es').Event<'transitionEnd', import('regl').Regl> &
|
|
301
300
|
import('pub-sub-es').Event<
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hema-to/regl-scatterplot",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.16.0-hemato.0",
|
|
4
4
|
"description": "A WebGL-Powered Scalable Interactive Scatter Plot Library",
|
|
5
5
|
"author": "Fritz Lekschas",
|
|
6
6
|
"keywords": [
|
|
@@ -39,10 +39,10 @@
|
|
|
39
39
|
"coverage": "vitest run --coverage"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@flekschas/utils": "^0.
|
|
42
|
+
"@flekschas/utils": "^0.33.0",
|
|
43
43
|
"dom-2d-camera": "^2.2.6",
|
|
44
|
-
"earcut": "^3.0.
|
|
45
|
-
"gl-matrix": "~3.4.
|
|
44
|
+
"earcut": "^3.0.2",
|
|
45
|
+
"gl-matrix": "~3.4.4",
|
|
46
46
|
"pub-sub-es": "~3.0.0",
|
|
47
47
|
"regl": "~2.1.1",
|
|
48
48
|
"regl-line": "~1.1.1"
|
|
@@ -52,49 +52,49 @@
|
|
|
52
52
|
"regl": "~2.1.1"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
|
-
"@babel/core": "^7.
|
|
56
|
-
"@babel/plugin-transform-regenerator": "^7.
|
|
55
|
+
"@babel/core": "^7.28.6",
|
|
56
|
+
"@babel/plugin-transform-regenerator": "^7.28.6",
|
|
57
57
|
"@babel/polyfill": "^7.12.1",
|
|
58
|
-
"@babel/preset-env": "^7.
|
|
59
|
-
"@biomejs/biome": "^
|
|
60
|
-
"@rollup/plugin-babel": "^6.0
|
|
61
|
-
"@rollup/plugin-commonjs": "^
|
|
58
|
+
"@babel/preset-env": "^7.28.6",
|
|
59
|
+
"@biomejs/biome": "^2.3.11",
|
|
60
|
+
"@rollup/plugin-babel": "^6.1.0",
|
|
61
|
+
"@rollup/plugin-commonjs": "^29.0.0",
|
|
62
62
|
"@rollup/plugin-json": "^6.0.0",
|
|
63
|
-
"@rollup/plugin-node-resolve": "^
|
|
64
|
-
"@rollup/plugin-terser": "^0.
|
|
63
|
+
"@rollup/plugin-node-resolve": "^16.0.3",
|
|
64
|
+
"@rollup/plugin-terser": "^1.0.0",
|
|
65
65
|
"@tweakpane/core": "^2.0.5",
|
|
66
|
-
"@types/d3-scale": "^4.0.
|
|
67
|
-
"@types/node": "^
|
|
68
|
-
"@vitest/browser": "^
|
|
69
|
-
"@vitest/
|
|
70
|
-
"
|
|
71
|
-
"
|
|
66
|
+
"@types/d3-scale": "^4.0.9",
|
|
67
|
+
"@types/node": "^25.0.8",
|
|
68
|
+
"@vitest/browser": "^4.0.17",
|
|
69
|
+
"@vitest/browser-playwright": "^4.0.17",
|
|
70
|
+
"@vitest/coverage-v8": "^4.0.17",
|
|
71
|
+
"apache-arrow": "^21.1.0",
|
|
72
72
|
"d3-axis": "^3.0.0",
|
|
73
73
|
"d3-random": "^3.0.1",
|
|
74
74
|
"d3-scale": "^4.0.2",
|
|
75
75
|
"d3-selection": "^3.0.0",
|
|
76
76
|
"esm": "^3.2.25",
|
|
77
|
-
"gh-pages": "^6.
|
|
77
|
+
"gh-pages": "^6.3.0",
|
|
78
78
|
"merge": "^2.1.1",
|
|
79
|
-
"playwright": "^1.
|
|
80
|
-
"rollup": "^4.
|
|
79
|
+
"playwright": "^1.57.0",
|
|
80
|
+
"rollup": "^4.55.1",
|
|
81
81
|
"rollup-plugin-filesize": "^10.0.0",
|
|
82
82
|
"tweakpane": "^4.0.5",
|
|
83
|
-
"typescript": "~5.
|
|
84
|
-
"vite": "^
|
|
83
|
+
"typescript": "~5.9.3",
|
|
84
|
+
"vite": "^7.3.1",
|
|
85
85
|
"vite-plugin-virtual-html-template": "^1.1.0",
|
|
86
|
-
"vitest": "^
|
|
86
|
+
"vitest": "^4.0.17"
|
|
87
87
|
},
|
|
88
88
|
"engines": {
|
|
89
89
|
"npm": ">=7.0.0",
|
|
90
90
|
"node": ">=20.0.0"
|
|
91
91
|
},
|
|
92
|
-
"homepage": "https://github.com/
|
|
92
|
+
"homepage": "https://github.com/hema-to/regl-scatterplot",
|
|
93
93
|
"repository": {
|
|
94
94
|
"type": "git",
|
|
95
|
-
"url": "git://github.com/
|
|
95
|
+
"url": "git+https://github.com/hema-to/regl-scatterplot.git"
|
|
96
96
|
},
|
|
97
97
|
"bugs": {
|
|
98
|
-
"url": "https://github.com/
|
|
98
|
+
"url": "https://github.com/hema-to/regl-scatterplot/issues"
|
|
99
99
|
}
|
|
100
100
|
}
|
package/src/constants.js
CHANGED
|
@@ -112,6 +112,7 @@ export const DEFAULT_POINT_SIZE = 6;
|
|
|
112
112
|
export const DEFAULT_POINT_SIZE_SELECTED = 2;
|
|
113
113
|
export const DEFAULT_POINT_OUTLINE_WIDTH = 2;
|
|
114
114
|
export const DEFAULT_SIZE_BY = null;
|
|
115
|
+
export const DEFAULT_POINT_ORDER = null;
|
|
115
116
|
export const DEFAULT_POINT_CONNECTION_SIZE = 2;
|
|
116
117
|
export const DEFAULT_POINT_CONNECTION_SIZE_ACTIVE = 2;
|
|
117
118
|
export const DEFAULT_POINT_CONNECTION_SIZE_BY = null;
|
package/src/index.js
CHANGED
|
@@ -11,21 +11,9 @@ import earcut from 'earcut';
|
|
|
11
11
|
import { mat4, vec4 } from 'gl-matrix';
|
|
12
12
|
import createPubSub from 'pub-sub-es';
|
|
13
13
|
import createLine from 'regl-line';
|
|
14
|
-
|
|
15
|
-
import createKdbush from './kdbush.js';
|
|
16
|
-
import createLassoManager from './lasso-manager/index.js';
|
|
17
|
-
import createRenderer from './renderer.js';
|
|
18
|
-
|
|
14
|
+
import { version } from '../package.json';
|
|
19
15
|
import BG_FS from './bg.fs';
|
|
20
16
|
import BG_VS from './bg.vs';
|
|
21
|
-
import POINT_SIMPLE_FS from './point-simple.fs';
|
|
22
|
-
import POINT_UPDATE_FS from './point-update.fs';
|
|
23
|
-
import POINT_UPDATE_VS from './point-update.vs';
|
|
24
|
-
import POINT_FS from './point.fs';
|
|
25
|
-
import createVertexShader from './point.vs';
|
|
26
|
-
|
|
27
|
-
import createSplineCurve from './spline-curve.js';
|
|
28
|
-
|
|
29
17
|
import {
|
|
30
18
|
AUTO,
|
|
31
19
|
CATEGORICAL,
|
|
@@ -88,6 +76,7 @@ import {
|
|
|
88
76
|
DEFAULT_POINT_CONNECTION_SIZE,
|
|
89
77
|
DEFAULT_POINT_CONNECTION_SIZE_ACTIVE,
|
|
90
78
|
DEFAULT_POINT_CONNECTION_SIZE_BY,
|
|
79
|
+
DEFAULT_POINT_ORDER,
|
|
91
80
|
DEFAULT_POINT_OUTLINE_WIDTH,
|
|
92
81
|
DEFAULT_POINT_SCALE_MODE,
|
|
93
82
|
DEFAULT_POINT_SIZE,
|
|
@@ -107,35 +96,43 @@ import {
|
|
|
107
96
|
ERROR_IS_DRAWING,
|
|
108
97
|
ERROR_POINTS_NOT_DRAWN,
|
|
109
98
|
FLOAT_BYTES,
|
|
110
|
-
KEYS,
|
|
111
|
-
KEY_ACTIONS,
|
|
112
99
|
KEY_ACTION_INTERSECT,
|
|
113
100
|
KEY_ACTION_LASSO,
|
|
114
101
|
KEY_ACTION_MERGE,
|
|
115
102
|
KEY_ACTION_REMOVE,
|
|
116
103
|
KEY_ACTION_ROTATE,
|
|
104
|
+
KEY_ACTIONS,
|
|
117
105
|
KEY_ALT,
|
|
118
106
|
KEY_CMD,
|
|
119
107
|
KEY_CTRL,
|
|
120
108
|
KEY_META,
|
|
121
109
|
KEY_SHIFT,
|
|
110
|
+
KEYS,
|
|
122
111
|
LASSO_BRUSH_MIN_MIN_DIST,
|
|
123
112
|
LASSO_CLEAR_EVENTS,
|
|
124
113
|
LASSO_CLEAR_ON_DESELECT,
|
|
125
114
|
LASSO_CLEAR_ON_END,
|
|
126
115
|
LONG_CLICK_TIME,
|
|
127
116
|
MIN_POINT_SIZE,
|
|
128
|
-
MOUSE_MODES,
|
|
129
117
|
MOUSE_MODE_LASSO,
|
|
130
118
|
MOUSE_MODE_PANZOOM,
|
|
131
119
|
MOUSE_MODE_ROTATE,
|
|
120
|
+
MOUSE_MODES,
|
|
132
121
|
SINGLE_CLICK_DELAY,
|
|
133
122
|
SKIP_DEPRECATION_VALUE_TRANSLATION,
|
|
134
123
|
VALUE_ZW_DATA_TYPES,
|
|
135
124
|
W_NAMES,
|
|
136
125
|
Z_NAMES,
|
|
137
126
|
} from './constants.js';
|
|
138
|
-
|
|
127
|
+
import createKdbush, { kdbushFrom } from './kdbush.js';
|
|
128
|
+
import createLassoManager from './lasso-manager/index.js';
|
|
129
|
+
import POINT_FS from './point.fs';
|
|
130
|
+
import createVertexShader from './point.vs';
|
|
131
|
+
import POINT_SIMPLE_FS from './point-simple.fs';
|
|
132
|
+
import POINT_UPDATE_FS from './point-update.fs';
|
|
133
|
+
import POINT_UPDATE_VS from './point-update.vs';
|
|
134
|
+
import createRenderer from './renderer.js';
|
|
135
|
+
import createSplineCurve from './spline-curve.js';
|
|
139
136
|
import {
|
|
140
137
|
checkReglExtensions as checkSupport,
|
|
141
138
|
clip,
|
|
@@ -150,7 +147,7 @@ import {
|
|
|
150
147
|
isHorizontalLine,
|
|
151
148
|
isMultipleColors,
|
|
152
149
|
isPointInPolygon,
|
|
153
|
-
|
|
150
|
+
isPolygonAnnotation,
|
|
154
151
|
isPositiveNumber,
|
|
155
152
|
isRect,
|
|
156
153
|
isSameRgbas,
|
|
@@ -158,16 +155,16 @@ import {
|
|
|
158
155
|
isString,
|
|
159
156
|
isValidBBox,
|
|
160
157
|
isVerticalLine,
|
|
158
|
+
isVertices,
|
|
161
159
|
limit,
|
|
162
160
|
max,
|
|
163
161
|
min,
|
|
164
162
|
rgbBrightness,
|
|
165
163
|
toArrayOrientedPoints,
|
|
166
164
|
toRgba,
|
|
165
|
+
verticesToPolygon,
|
|
167
166
|
} from './utils.js';
|
|
168
167
|
|
|
169
|
-
import { version } from '../package.json';
|
|
170
|
-
|
|
171
168
|
const deprecations = {
|
|
172
169
|
showRecticle: {
|
|
173
170
|
replacement: 'showReticle',
|
|
@@ -320,6 +317,7 @@ const createScatterplot = (
|
|
|
320
317
|
opacityInactiveMax = DEFAULT_OPACITY_INACTIVE_MAX,
|
|
321
318
|
opacityInactiveScale = DEFAULT_OPACITY_INACTIVE_SCALE,
|
|
322
319
|
sizeBy = DEFAULT_SIZE_BY,
|
|
320
|
+
pointOrder = DEFAULT_POINT_ORDER,
|
|
323
321
|
pointScaleMode = DEFAULT_POINT_SCALE_MODE,
|
|
324
322
|
height = DEFAULT_HEIGHT,
|
|
325
323
|
width = DEFAULT_WIDTH,
|
|
@@ -395,9 +393,7 @@ const createScatterplot = (
|
|
|
395
393
|
let pointConnections;
|
|
396
394
|
let pointConnectionMap;
|
|
397
395
|
let computingPointConnectionCurves;
|
|
398
|
-
// biome-ignore lint/style/useNamingConvention: HLine stands for HorizontalLine
|
|
399
396
|
let reticleHLine;
|
|
400
|
-
// biome-ignore lint/style/useNamingConvention: VLine stands for VerticalLine
|
|
401
397
|
let reticleVLine;
|
|
402
398
|
let computedPointSizeMouseDetection;
|
|
403
399
|
let lassoInitiatorTimeout;
|
|
@@ -530,6 +526,8 @@ const createScatterplot = (
|
|
|
530
526
|
let normalPointsIndexBuffer; // Buffer holding the indices pointing to the correct texel
|
|
531
527
|
let selectedPointsIndexBuffer; // Used for pointing to the selected texels
|
|
532
528
|
let hoveredPointIndexBuffer; // Used for pointing to the hovered texels
|
|
529
|
+
let pointOrderIndices = null; // Validated ordered point indices (Int32Array or null)
|
|
530
|
+
let pointOrderIndex = null; // Float32Array of tex coords in pointOrder sequence
|
|
533
531
|
|
|
534
532
|
let cameraZoomTargetStart; // Stores the start (i.e., current) camera target for zooming
|
|
535
533
|
let cameraZoomTargetEnd; // Stores the end camera target for zooming
|
|
@@ -552,9 +550,7 @@ const createScatterplot = (
|
|
|
552
550
|
let isAnnotationsDrawn = false;
|
|
553
551
|
let isMouseOverCanvasChecked = false;
|
|
554
552
|
|
|
555
|
-
// biome-ignore lint/style/useNamingConvention: ZDate is not one word
|
|
556
553
|
let valueZDataType = CATEGORICAL;
|
|
557
|
-
// biome-ignore lint/style/useNamingConvention: WDate is not one word
|
|
558
554
|
let valueWDataType = CATEGORICAL;
|
|
559
555
|
|
|
560
556
|
/** @type{number|undefined} */
|
|
@@ -628,9 +624,7 @@ const createScatterplot = (
|
|
|
628
624
|
return points;
|
|
629
625
|
};
|
|
630
626
|
|
|
631
|
-
// biome-ignore lint/style/useNamingConvention: BBox stands for BoundingBox
|
|
632
627
|
const getPointsInBBox = (x0, y0, x1, y1) => {
|
|
633
|
-
// biome-ignore lint/style/useNamingConvention: BBox stands for BoundingBox
|
|
634
628
|
const pointsInBBox = spatialIndex.range(x0, y0, x1, y1);
|
|
635
629
|
if (isPointsFiltered) {
|
|
636
630
|
return pointsInBBox.filter((i) => filteredPointsSet.has(i));
|
|
@@ -645,7 +639,6 @@ const createScatterplot = (
|
|
|
645
639
|
const pointSizeNdc = getPointSizeNdc(4);
|
|
646
640
|
|
|
647
641
|
// Get all points within a close range
|
|
648
|
-
// biome-ignore lint/style/useNamingConvention: BBox stands for BoundingBox
|
|
649
642
|
const pointsInBBox = getPointsInBBox(
|
|
650
643
|
xNdc - pointSizeNdc,
|
|
651
644
|
yNdc - pointSizeNdc,
|
|
@@ -683,7 +676,6 @@ const createScatterplot = (
|
|
|
683
676
|
}
|
|
684
677
|
|
|
685
678
|
// ...to efficiently preselect potentially selected points
|
|
686
|
-
// biome-ignore lint/style/useNamingConvention: BBox stands for BoundingBox
|
|
687
679
|
const pointsInBBox = getPointsInBBox(...bBox);
|
|
688
680
|
// next we test each point in the bounding box if it is in the polygon too
|
|
689
681
|
const pointsInPolygon = [];
|
|
@@ -788,6 +780,40 @@ const createScatterplot = (
|
|
|
788
780
|
}
|
|
789
781
|
};
|
|
790
782
|
|
|
783
|
+
/**
|
|
784
|
+
* Convert vertices from data space to GL space
|
|
785
|
+
* @param {Array<[number, number]>} vertices - Vertices in data space
|
|
786
|
+
* @returns {number[] | null} Flat array of GL coordinates or null if scales not defined
|
|
787
|
+
*/
|
|
788
|
+
const verticesFromDataToGl = (vertices) => {
|
|
789
|
+
// Check if xScale/yScale are defined
|
|
790
|
+
if (!(xScale && yScale)) {
|
|
791
|
+
// biome-ignore lint/suspicious/noConsole: User warning for missing configuration
|
|
792
|
+
console.warn(
|
|
793
|
+
'xScale and yScale must be defined for programmatic lasso selection',
|
|
794
|
+
);
|
|
795
|
+
return null;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
const verticesGl = [];
|
|
799
|
+
for (const [x, y] of vertices) {
|
|
800
|
+
// Step 1: Data space → normalized [0, 1]
|
|
801
|
+
const xNorm = (x - xDomainStart) / xDomainSize;
|
|
802
|
+
const yNorm = (y - yDomainStart) / yDomainSize;
|
|
803
|
+
|
|
804
|
+
// Step 2: Normalized [0, 1] → NDC [-1, 1]
|
|
805
|
+
const xNdc = xNorm * 2 - 1;
|
|
806
|
+
const yNdc = yNorm * 2 - 1;
|
|
807
|
+
|
|
808
|
+
// Step 3: NDC → GL space (camera transform)
|
|
809
|
+
const [xGl, yGl] = getScatterGlPos(xNdc, yNdc);
|
|
810
|
+
|
|
811
|
+
verticesGl.push(xGl, yGl);
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
return verticesGl;
|
|
815
|
+
};
|
|
816
|
+
|
|
791
817
|
/**
|
|
792
818
|
* Select and highlight a set of points
|
|
793
819
|
* @param {number | number[]} pointIdxs
|
|
@@ -885,12 +911,53 @@ const createScatterplot = (
|
|
|
885
911
|
setPointConnectionColorState(selectedPoints, 1);
|
|
886
912
|
|
|
887
913
|
if (!preventEvent) {
|
|
888
|
-
pubSub.publish('select', { points: selectedPoints });
|
|
914
|
+
pubSub.publish('select', { points: selectedPoints, lassoMode });
|
|
889
915
|
}
|
|
890
916
|
|
|
891
917
|
draw = true;
|
|
892
918
|
};
|
|
893
919
|
|
|
920
|
+
/**
|
|
921
|
+
* Lasso a certain area and select contained points
|
|
922
|
+
* @param {[number, number][]} vertices - Lasso vertices in either data space (default) or GL space
|
|
923
|
+
* @param {import('./types').ScatterplotMethodOptions['lasso']}
|
|
924
|
+
*/
|
|
925
|
+
const lassoSelect = (
|
|
926
|
+
vertices,
|
|
927
|
+
{ merge = false, remove = false, isGl = false } = {},
|
|
928
|
+
) => {
|
|
929
|
+
if (!isVertices(vertices)) {
|
|
930
|
+
throw new Error(
|
|
931
|
+
'Lasso selection requires at least 3 vertices as [x, y] coordinate pairs',
|
|
932
|
+
);
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
const closedPolygon = verticesToPolygon(vertices);
|
|
936
|
+
|
|
937
|
+
let polygonGl;
|
|
938
|
+
let polygonGlFlat;
|
|
939
|
+
|
|
940
|
+
if (isGl) {
|
|
941
|
+
polygonGl = closedPolygon;
|
|
942
|
+
polygonGlFlat = closedPolygon.flat();
|
|
943
|
+
} else {
|
|
944
|
+
polygonGlFlat = verticesFromDataToGl(closedPolygon);
|
|
945
|
+
|
|
946
|
+
if (!polygonGlFlat) {
|
|
947
|
+
throw new Error(
|
|
948
|
+
'xScale and yScale must be defined to convert lasso vertices from data space to GL space',
|
|
949
|
+
);
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
polygonGl = [];
|
|
953
|
+
for (let i = 0; i < polygonGlFlat.length; i += 2) {
|
|
954
|
+
polygonGl.push([polygonGlFlat[i], polygonGlFlat[i + 1]]);
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
lassoEnd(polygonGl, polygonGlFlat, { merge, remove });
|
|
959
|
+
};
|
|
960
|
+
|
|
894
961
|
/**
|
|
895
962
|
* @param {number} point
|
|
896
963
|
* @param {import('./types').ScatterplotMethodOptions['hover']} options
|
|
@@ -1567,6 +1634,44 @@ const createScatterplot = (
|
|
|
1567
1634
|
const setSizeBy = (type) => {
|
|
1568
1635
|
sizeBy = getEncodingType(type, DEFAULT_SIZE_BY);
|
|
1569
1636
|
};
|
|
1637
|
+
const setPointOrder = (newPointOrder) => {
|
|
1638
|
+
if (newPointOrder === null || newPointOrder === undefined) {
|
|
1639
|
+
pointOrder = null;
|
|
1640
|
+
} else if (Array.isArray(newPointOrder) || ArrayBuffer.isView(newPointOrder)) {
|
|
1641
|
+
pointOrder = newPointOrder;
|
|
1642
|
+
} else {
|
|
1643
|
+
return;
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
if (isPointsDrawn) {
|
|
1647
|
+
computePointOrderIndex();
|
|
1648
|
+
if (isPointsFiltered) {
|
|
1649
|
+
// Re-apply filter respecting the new point order
|
|
1650
|
+
const filteredPointsBuffer = [];
|
|
1651
|
+
if (pointOrderIndices !== null) {
|
|
1652
|
+
for (let i = 0; i < pointOrderIndices.length; i++) {
|
|
1653
|
+
if (filteredPointsSet.has(pointOrderIndices[i])) {
|
|
1654
|
+
filteredPointsBuffer.push.apply(
|
|
1655
|
+
filteredPointsBuffer,
|
|
1656
|
+
indexToStateTexCoord(pointOrderIndices[i]),
|
|
1657
|
+
);
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
} else {
|
|
1661
|
+
const sortedFiltered = insertionSort([...filteredPointsSet]);
|
|
1662
|
+
for (const idx of sortedFiltered) {
|
|
1663
|
+
filteredPointsBuffer.push.apply(
|
|
1664
|
+
filteredPointsBuffer,
|
|
1665
|
+
indexToStateTexCoord(idx),
|
|
1666
|
+
);
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
normalPointsIndexBuffer.subdata(filteredPointsBuffer);
|
|
1670
|
+
} else {
|
|
1671
|
+
normalPointsIndexBuffer.subdata(getEffectivePointIndex(numPoints));
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
};
|
|
1570
1675
|
const setPointConnectionColorBy = (type) => {
|
|
1571
1676
|
pointConnectionColorBy = getEncodingType(
|
|
1572
1677
|
type,
|
|
@@ -1740,10 +1845,8 @@ const createScatterplot = (
|
|
|
1740
1845
|
blend: {
|
|
1741
1846
|
enable: !disableAlphaBlending,
|
|
1742
1847
|
func: {
|
|
1743
|
-
// biome-ignore lint/style/useNamingConvention: Regl specific
|
|
1744
1848
|
srcRGB: 'src alpha',
|
|
1745
1849
|
srcAlpha: 'one',
|
|
1746
|
-
// biome-ignore lint/style/useNamingConvention: Regl specific
|
|
1747
1850
|
dstRGB: 'one minus src alpha',
|
|
1748
1851
|
dstAlpha: 'one minus src alpha',
|
|
1749
1852
|
},
|
|
@@ -1884,10 +1987,8 @@ const createScatterplot = (
|
|
|
1884
1987
|
blend: {
|
|
1885
1988
|
enable: true,
|
|
1886
1989
|
func: {
|
|
1887
|
-
// biome-ignore lint/style/useNamingConvention: Regl specific
|
|
1888
1990
|
srcRGB: 'src alpha',
|
|
1889
1991
|
srcAlpha: 'one',
|
|
1890
|
-
// biome-ignore lint/style/useNamingConvention: Regl specific
|
|
1891
1992
|
dstRGB: 'one minus src alpha',
|
|
1892
1993
|
dstAlpha: 'one minus src alpha',
|
|
1893
1994
|
},
|
|
@@ -1965,6 +2066,51 @@ const createScatterplot = (
|
|
|
1965
2066
|
return index;
|
|
1966
2067
|
};
|
|
1967
2068
|
|
|
2069
|
+
const computePointOrderIndex = () => {
|
|
2070
|
+
if (pointOrder === null) {
|
|
2071
|
+
pointOrderIndices = null;
|
|
2072
|
+
pointOrderIndex = null;
|
|
2073
|
+
return;
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
const includedSet = new Set();
|
|
2077
|
+
const orderedIndices = [];
|
|
2078
|
+
|
|
2079
|
+
for (let i = 0; i < pointOrder.length; i++) {
|
|
2080
|
+
const idx = pointOrder[i];
|
|
2081
|
+
if (
|
|
2082
|
+
Number.isFinite(idx) &&
|
|
2083
|
+
idx >= 0 &&
|
|
2084
|
+
idx < numPoints &&
|
|
2085
|
+
!includedSet.has(idx)
|
|
2086
|
+
) {
|
|
2087
|
+
orderedIndices.push(idx);
|
|
2088
|
+
includedSet.add(idx);
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
// Append any missing indices in sequential order
|
|
2093
|
+
for (let i = 0; i < numPoints; i++) {
|
|
2094
|
+
if (!includedSet.has(i)) {
|
|
2095
|
+
orderedIndices.push(i);
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
|
|
2099
|
+
pointOrderIndices = orderedIndices;
|
|
2100
|
+
|
|
2101
|
+
pointOrderIndex = new Float32Array(orderedIndices.length * 2);
|
|
2102
|
+
let j = 0;
|
|
2103
|
+
for (let i = 0; i < orderedIndices.length; i++) {
|
|
2104
|
+
const texCoord = indexToStateTexCoord(orderedIndices[i]);
|
|
2105
|
+
pointOrderIndex[j] = texCoord[0];
|
|
2106
|
+
pointOrderIndex[j + 1] = texCoord[1];
|
|
2107
|
+
j += 2;
|
|
2108
|
+
}
|
|
2109
|
+
};
|
|
2110
|
+
|
|
2111
|
+
const getEffectivePointIndex = (count) =>
|
|
2112
|
+
pointOrderIndex !== null ? pointOrderIndex : createPointIndex(count);
|
|
2113
|
+
|
|
1968
2114
|
const createStateTexture = (newPoints, dataTypes = {}) => {
|
|
1969
2115
|
const numNewPoints = newPoints.length;
|
|
1970
2116
|
stateTexRes = Math.max(2, Math.ceil(Math.sqrt(numNewPoints)));
|
|
@@ -2056,9 +2202,18 @@ const createScatterplot = (
|
|
|
2056
2202
|
const preventFilterReset =
|
|
2057
2203
|
options?.preventFilterReset && newPoints.length === numPoints;
|
|
2058
2204
|
|
|
2205
|
+
const prevNumPoints = numPoints;
|
|
2059
2206
|
numPoints = newPoints.length;
|
|
2060
2207
|
numPointsInView = numPoints;
|
|
2061
2208
|
|
|
2209
|
+
// Reset pointOrder when re-drawing with different-length data
|
|
2210
|
+
// (ordering becomes meaningless). Skip on first draw (prevNumPoints === 0).
|
|
2211
|
+
if (prevNumPoints > 0 && numPoints !== prevNumPoints) {
|
|
2212
|
+
pointOrder = null;
|
|
2213
|
+
pointOrderIndices = null;
|
|
2214
|
+
pointOrderIndex = null;
|
|
2215
|
+
}
|
|
2216
|
+
|
|
2062
2217
|
if (stateTex) {
|
|
2063
2218
|
stateTex.destroy();
|
|
2064
2219
|
}
|
|
@@ -2068,10 +2223,11 @@ const createScatterplot = (
|
|
|
2068
2223
|
});
|
|
2069
2224
|
|
|
2070
2225
|
if (!preventFilterReset) {
|
|
2226
|
+
computePointOrderIndex();
|
|
2071
2227
|
normalPointsIndexBuffer({
|
|
2072
2228
|
usage: 'static',
|
|
2073
2229
|
type: 'float',
|
|
2074
|
-
data:
|
|
2230
|
+
data: getEffectivePointIndex(numPoints),
|
|
2075
2231
|
});
|
|
2076
2232
|
}
|
|
2077
2233
|
|
|
@@ -2312,7 +2468,7 @@ const createScatterplot = (
|
|
|
2312
2468
|
const unfilter = ({ preventEvent = false } = {}) => {
|
|
2313
2469
|
isPointsFiltered = false;
|
|
2314
2470
|
filteredPointsSet.clear();
|
|
2315
|
-
normalPointsIndexBuffer.subdata(
|
|
2471
|
+
normalPointsIndexBuffer.subdata(getEffectivePointIndex(numPoints));
|
|
2316
2472
|
|
|
2317
2473
|
return new Promise((resolve) => {
|
|
2318
2474
|
const finish = () => {
|
|
@@ -2371,9 +2527,20 @@ const createScatterplot = (
|
|
|
2371
2527
|
}
|
|
2372
2528
|
}
|
|
2373
2529
|
|
|
2374
|
-
|
|
2530
|
+
let orderedFilteredPoints;
|
|
2531
|
+
if (pointOrderIndices !== null) {
|
|
2532
|
+
// Maintain the custom point order within the filtered set
|
|
2533
|
+
orderedFilteredPoints = [];
|
|
2534
|
+
for (let i = 0; i < pointOrderIndices.length; i++) {
|
|
2535
|
+
if (filteredPointsSet.has(pointOrderIndices[i])) {
|
|
2536
|
+
orderedFilteredPoints.push(pointOrderIndices[i]);
|
|
2537
|
+
}
|
|
2538
|
+
}
|
|
2539
|
+
} else {
|
|
2540
|
+
orderedFilteredPoints = insertionSort([...filteredPoints]);
|
|
2541
|
+
}
|
|
2375
2542
|
|
|
2376
|
-
for (const pointIdx of
|
|
2543
|
+
for (const pointIdx of orderedFilteredPoints) {
|
|
2377
2544
|
filteredPointsBuffer.push.apply(
|
|
2378
2545
|
filteredPointsBuffer,
|
|
2379
2546
|
indexToStateTexCoord(pointIdx),
|
|
@@ -2760,7 +2927,7 @@ const createScatterplot = (
|
|
|
2760
2927
|
continue;
|
|
2761
2928
|
}
|
|
2762
2929
|
|
|
2763
|
-
if (
|
|
2930
|
+
if (isPolygonAnnotation(annotation)) {
|
|
2764
2931
|
newPoints.push(annotation.vertices.flatMap(identity));
|
|
2765
2932
|
addColorAndWidth(annotation);
|
|
2766
2933
|
}
|
|
@@ -2801,7 +2968,6 @@ const createScatterplot = (
|
|
|
2801
2968
|
* @param {number[]} pointIdxs - A list of point indices
|
|
2802
2969
|
* @returns {import('./types').Rect} The bounding box
|
|
2803
2970
|
*/
|
|
2804
|
-
// biome-ignore lint/style/useNamingConvention: BBox stands for BoundingBox
|
|
2805
2971
|
const getBBoxOfPoints = (pointIdxs) => {
|
|
2806
2972
|
let xMin = Number.POSITIVE_INFINITY;
|
|
2807
2973
|
let xMax = Number.NEGATIVE_INFINITY;
|
|
@@ -2838,7 +3004,6 @@ const createScatterplot = (
|
|
|
2838
3004
|
// Vertical field of view
|
|
2839
3005
|
// The Arc Tangent is based on the original camera position. Otherwise
|
|
2840
3006
|
// we would have to do `Math.atan(1 / camera.view[5])`
|
|
2841
|
-
// biome-ignore lint/style/useNamingConvention: FOV stands for field of view
|
|
2842
3007
|
const vFOV = 2 * Math.atan(1);
|
|
2843
3008
|
|
|
2844
3009
|
const aspectRatio = viewAspectRatio / dataAspectRatio;
|
|
@@ -3238,7 +3403,6 @@ const createScatterplot = (
|
|
|
3238
3403
|
reticleVLine.setStyle({ color: reticleColor });
|
|
3239
3404
|
};
|
|
3240
3405
|
|
|
3241
|
-
// biome-ignore lint/style/useNamingConvention: XScale are two words
|
|
3242
3406
|
const setXScale = (newXScale) => {
|
|
3243
3407
|
if (!newXScale) {
|
|
3244
3408
|
return;
|
|
@@ -3251,7 +3415,6 @@ const createScatterplot = (
|
|
|
3251
3415
|
updateScales();
|
|
3252
3416
|
};
|
|
3253
3417
|
|
|
3254
|
-
// biome-ignore lint/style/useNamingConvention: YScale are two words
|
|
3255
3418
|
const setYScale = (newYScale) => {
|
|
3256
3419
|
if (!newYScale) {
|
|
3257
3420
|
return;
|
|
@@ -3425,7 +3588,6 @@ const createScatterplot = (
|
|
|
3425
3588
|
annotationLineWidth = +newAnnotationLineWidth;
|
|
3426
3589
|
};
|
|
3427
3590
|
|
|
3428
|
-
// biome-ignore lint/style/useNamingConvention: HVLine stands for horizontal vertical line
|
|
3429
3591
|
const setAnnotationHVLineLimit = (newAnnotationHVLineLimit) => {
|
|
3430
3592
|
annotationHVLineLimit = +newAnnotationHVLineLimit;
|
|
3431
3593
|
};
|
|
@@ -3502,6 +3664,10 @@ const createScatterplot = (
|
|
|
3502
3664
|
return sizeBy;
|
|
3503
3665
|
}
|
|
3504
3666
|
|
|
3667
|
+
if (property === 'pointOrder') {
|
|
3668
|
+
return pointOrder !== null ? [...pointOrder] : null;
|
|
3669
|
+
}
|
|
3670
|
+
|
|
3505
3671
|
if (property === 'deselectOnDblClick') {
|
|
3506
3672
|
return deselectOnDblClick;
|
|
3507
3673
|
}
|
|
@@ -3888,6 +4054,10 @@ const createScatterplot = (
|
|
|
3888
4054
|
setSizeBy(properties.sizeBy);
|
|
3889
4055
|
}
|
|
3890
4056
|
|
|
4057
|
+
if (properties.pointOrder !== undefined) {
|
|
4058
|
+
setPointOrder(properties.pointOrder);
|
|
4059
|
+
}
|
|
4060
|
+
|
|
3891
4061
|
if (properties.opacity !== undefined) {
|
|
3892
4062
|
setOpacity(properties.opacity);
|
|
3893
4063
|
}
|
|
@@ -4587,6 +4757,9 @@ const createScatterplot = (
|
|
|
4587
4757
|
init();
|
|
4588
4758
|
|
|
4589
4759
|
return {
|
|
4760
|
+
attachSpatialIndex: (buffer) => {
|
|
4761
|
+
spatialIndex = kdbushFrom(buffer);
|
|
4762
|
+
},
|
|
4590
4763
|
/**
|
|
4591
4764
|
* Get whether the browser supports all necessary WebGL features
|
|
4592
4765
|
* @return {boolean} If `true` the browser supports all necessary WebGL features
|
|
@@ -4610,6 +4783,7 @@ const createScatterplot = (
|
|
|
4610
4783
|
get,
|
|
4611
4784
|
getScreenPosition,
|
|
4612
4785
|
hover,
|
|
4786
|
+
lassoSelect,
|
|
4613
4787
|
redraw,
|
|
4614
4788
|
refresh: renderer.refresh,
|
|
4615
4789
|
reset: withDraw(reset),
|