@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.
package/dist/types.d.ts CHANGED
@@ -17,7 +17,6 @@ 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<'select' | 'focus', { points: number[]; lassoMode: LassoOptions["mode"]; }> &
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.14.1-hemato.3",
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.32.2",
42
+ "@flekschas/utils": "^0.33.0",
43
43
  "dom-2d-camera": "^2.2.6",
44
- "earcut": "^3.0.1",
45
- "gl-matrix": "~3.4.3",
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.26.0",
56
- "@babel/plugin-transform-regenerator": "^7.25.9",
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.26.0",
59
- "@biomejs/biome": "^1.9.4",
60
- "@rollup/plugin-babel": "^6.0.3",
61
- "@rollup/plugin-commonjs": "^28.0.1",
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": "^15.3.0",
64
- "@rollup/plugin-terser": "^0.4.3",
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.6",
67
- "@types/node": "^22.9.3",
68
- "@vitest/browser": "^3.0.9",
69
- "@vitest/coverage-v8": "^3.0.9",
70
- "apache-arrow": "^18.0.0",
71
- "browser-env": "^3.3.0",
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.2.0",
77
+ "gh-pages": "^6.3.0",
78
78
  "merge": "^2.1.1",
79
- "playwright": "^1.49.0",
80
- "rollup": "^4.27.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.7.2",
84
- "vite": "^6.2.3",
83
+ "typescript": "~5.9.3",
84
+ "vite": "^7.3.1",
85
85
  "vite-plugin-virtual-html-template": "^1.1.0",
86
- "vitest": "^3.0.9"
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/flekschas/regl-scatterplot",
92
+ "homepage": "https://github.com/hema-to/regl-scatterplot",
93
93
  "repository": {
94
94
  "type": "git",
95
- "url": "git://github.com/flekschas/regl-scatterplot.git"
95
+ "url": "git+https://github.com/hema-to/regl-scatterplot.git"
96
96
  },
97
97
  "bugs": {
98
- "url": "https://github.com/flekschas/regl-scatterplot/issues"
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
- isPolygon,
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
@@ -891,6 +917,47 @@ const createScatterplot = (
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: createPointIndex(numPoints),
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(createPointIndex(numPoints));
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
- const sortedFilteredPoints = insertionSort([...filteredPoints]);
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 sortedFilteredPoints) {
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 (isPolygon(annotation)) {
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),
@@ -6,7 +6,6 @@ export default () => {
6
6
  self.postMessage({ error: new Error('Invalid point data') });
7
7
  }
8
8
 
9
- // biome-ignore lint/correctness/noUndeclaredVariables: KDBush is made available during compilation
10
9
  const index = new KDBush(points.length, event.data.nodeSize);
11
10
 
12
11
  for (const [x, y] of points) {