Skip to content

Commit 58689a4

Browse files
committed
refactor: add dedicated lassoSelect() method per code review
- Replace overloaded select(polygon) with lassoSelect(vertices) API - Add isVertices() validation checking all vertices - Add verticesToPolygon() helper to auto-close polygons - Rename polygonDataToGl to verticesFromDataToGl for clarity - Addtests - Update programmatic-lasso example to use new API - Update changelog.md
1 parent ab7553c commit 58689a4

File tree

5 files changed

+401
-51
lines changed

5 files changed

+401
-51
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
## 1.15.0
22

3-
- Feat: add programmatic lasso selection API allowing `select()` to accept polygon vertices in data space. This enables automated point selection without manual interaction. Works with `merge` and `remove` options and requires `xScale` and `yScale` to be defined.
3+
- Feat: add programmatic lasso selection API via new `lassoSelect()` method that accept a polygon in either data or GL space. This enables automated point selection without manual interaction. Supports `merge` and `remove` options. Note, vertices in data space requires `xScale` and `yScale` to be defined.
44

55
## 1.14.1
66

example/programmatic-lasso.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ const createButton = (label, onClick, wide = false) => {
189189
// Button 1: Select bottom-left triangle
190190
buttonContainer.appendChild(
191191
createButton('△ Bottom-Left', () => {
192-
scatterplot.select([
192+
scatterplot.lassoSelect([
193193
[10, 10],
194194
[40, 10],
195195
[10, 40],
@@ -214,14 +214,14 @@ buttonContainer.appendChild(
214214
]);
215215
}
216216

217-
scatterplot.select(polygon);
217+
scatterplot.lassoSelect(polygon);
218218
})
219219
);
220220

221221
// Button 3: Select center rectangle
222222
buttonContainer.appendChild(
223223
createButton('▭ Center', () => {
224-
scatterplot.select([
224+
scatterplot.lassoSelect([
225225
[30, 30],
226226
[70, 30],
227227
[70, 70],
@@ -233,7 +233,7 @@ buttonContainer.appendChild(
233233
// Button 4: Add diagonal stripe (merge)
234234
buttonContainer.appendChild(
235235
createButton('+ Diagonal (Merge)', () => {
236-
scatterplot.select(
236+
scatterplot.lassoSelect(
237237
[
238238
[0, 40],
239239
[60, 100],
@@ -248,7 +248,7 @@ buttonContainer.appendChild(
248248
// Button 5: Remove center square
249249
buttonContainer.appendChild(
250250
createButton('− Center (Remove)', () => {
251-
scatterplot.select(
251+
scatterplot.lassoSelect(
252252
[
253253
[40, 40],
254254
[60, 40],
@@ -279,7 +279,7 @@ buttonContainer.appendChild(
279279
]);
280280
}
281281

282-
scatterplot.select(polygon);
282+
scatterplot.lassoSelect(polygon);
283283
})
284284
);
285285

src/index.js

Lines changed: 54 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -148,19 +148,22 @@ import {
148148
isHorizontalLine,
149149
isMultipleColors,
150150
isPointInPolygon,
151+
isPolygonAnnotation,
151152
isPositiveNumber,
152153
isRect,
153154
isSameRgbas,
154155
isStrictlyPositiveNumber,
155156
isString,
156157
isValidBBox,
157158
isVerticalLine,
159+
isVertices,
158160
limit,
159161
max,
160162
min,
161163
rgbBrightness,
162164
toArrayOrientedPoints,
163165
toRgba,
166+
verticesToPolygon,
164167
} from './utils.js';
165168

166169
import { version } from '../package.json';
@@ -785,27 +788,11 @@ const createScatterplot = (
785788
};
786789

787790
/**
788-
* Check if argument is a polygon (array of [x, y] coordinate pairs)
789-
* @param {any} arg - The argument to check
790-
* @returns {boolean} True if argument is a valid polygon
791-
*/
792-
const isPolygon = (arg) => {
793-
return (
794-
Array.isArray(arg) &&
795-
arg.length >= 3 &&
796-
Array.isArray(arg[0]) &&
797-
arg[0].length === 2 &&
798-
typeof arg[0][0] === 'number' &&
799-
typeof arg[0][1] === 'number'
800-
);
801-
};
802-
803-
/**
804-
* Convert polygon from data space to GL space for lasso selection
805-
* @param {Array<[number, number]>} polygonData - Polygon vertices in data space
791+
* Convert vertices from data space to GL space
792+
* @param {Array<[number, number]>} vertices - Vertices in data space
806793
* @returns {number[] | null} Flat array of GL coordinates or null if scales not defined
807794
*/
808-
const polygonDataToGl = (polygonData) => {
795+
const verticesFromDataToGl = (vertices) => {
809796
// Check if xScale/yScale are defined
810797
if (!(xScale && yScale)) {
811798
// biome-ignore lint/suspicious/noConsole: User warning for missing configuration
@@ -815,8 +802,8 @@ const createScatterplot = (
815802
return null;
816803
}
817804

818-
const polygonGl = [];
819-
for (const [x, y] of polygonData) {
805+
const verticesGl = [];
806+
for (const [x, y] of vertices) {
820807
// Step 1: Data space → normalized [0, 1]
821808
const xNorm = (x - xDomainStart) / xDomainSize;
822809
const yNorm = (y - yDomainStart) / yDomainSize;
@@ -828,39 +815,21 @@ const createScatterplot = (
828815
// Step 3: NDC → GL space (camera transform)
829816
const [xGl, yGl] = getScatterGlPos(xNdc, yNdc);
830817

831-
polygonGl.push(xGl, yGl);
818+
verticesGl.push(xGl, yGl);
832819
}
833820

834-
return polygonGl;
821+
return verticesGl;
835822
};
836823

837824
/**
838825
* Select and highlight a set of points
839-
* @param {number | number[] | Array<[number, number]>} pointIdxs - Point indices or polygon vertices
826+
* @param {number | number[]} pointIdxs
840827
* @param {import('./types').ScatterplotMethodOptions['select']}
841828
*/
842829
const select = (
843830
pointIdxs,
844831
{ merge = false, remove = false, preventEvent = false } = {},
845832
) => {
846-
// Check if input is a polygon (array of [x, y] coordinate pairs)
847-
if (isPolygon(pointIdxs)) {
848-
// Convert polygon from data space to GL space
849-
const polygonGl = polygonDataToGl(pointIdxs);
850-
851-
if (!polygonGl) {
852-
// Scales not defined, cannot proceed
853-
return;
854-
}
855-
856-
// Find points within the polygon using existing lasso logic
857-
const pointsInPolygon = findPointsInLasso(polygonGl);
858-
859-
// Recursively call select with the found point indices
860-
select(pointsInPolygon, { merge, remove, preventEvent });
861-
return;
862-
}
863-
864833
const newSelectedPoints = Array.isArray(pointIdxs)
865834
? pointIdxs
866835
: [pointIdxs];
@@ -938,6 +907,47 @@ const createScatterplot = (
938907
draw = true;
939908
};
940909

910+
/**
911+
* Lasso a certain area and select contained points
912+
* @param {[number, number][]} vertices - Lasso vertices in either data space (default) or GL space
913+
* @param {import('./types').ScatterplotMethodOptions['lasso']}
914+
*/
915+
const lassoSelect = (
916+
vertices,
917+
{ merge = false, remove = false, isGl = false } = {},
918+
) => {
919+
if (!isVertices(vertices)) {
920+
throw new Error(
921+
'Lasso selection requires at least 3 vertices as [x, y] coordinate pairs',
922+
);
923+
}
924+
925+
const closedPolygon = verticesToPolygon(vertices);
926+
927+
let polygonGl;
928+
let polygonGlFlat;
929+
930+
if (isGl) {
931+
polygonGl = closedPolygon;
932+
polygonGlFlat = closedPolygon.flat();
933+
} else {
934+
polygonGlFlat = verticesFromDataToGl(closedPolygon);
935+
936+
if (!polygonGlFlat) {
937+
throw new Error(
938+
'xScale and yScale must be defined to convert lasso vertices from data space to GL space',
939+
);
940+
}
941+
942+
polygonGl = [];
943+
for (let i = 0; i < polygonGlFlat.length; i += 2) {
944+
polygonGl.push([polygonGlFlat[i], polygonGlFlat[i + 1]]);
945+
}
946+
}
947+
948+
lassoEnd(polygonGl, polygonGlFlat, { merge, remove });
949+
};
950+
941951
/**
942952
* @param {number} point
943953
* @param {import('./types').ScatterplotMethodOptions['hover']} options
@@ -2797,7 +2807,7 @@ const createScatterplot = (
27972807
continue;
27982808
}
27992809

2800-
if (isPolygon(annotation)) {
2810+
if (isPolygonAnnotation(annotation)) {
28012811
newPoints.push(annotation.vertices.flatMap(identity));
28022812
addColorAndWidth(annotation);
28032813
}
@@ -4639,6 +4649,7 @@ const createScatterplot = (
46394649
get,
46404650
getScreenPosition,
46414651
hover,
4652+
lassoSelect,
46424653
redraw,
46434654
refresh: renderer.refresh,
46444655
reset: withDraw(reset),

src/utils.js

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -578,9 +578,48 @@ export const isRect = (annotation) =>
578578
Number.isFinite(annotation.x2) &&
579579
Number.isFinite(annotation.x2);
580580

581-
export const isPolygon = (annotation) =>
581+
export const isPolygonAnnotation = (annotation) =>
582582
'vertices' in annotation && annotation.vertices.length > 1;
583583

584+
/**
585+
* Check if an array is a valid list of 2D vertices
586+
* @param {any} arg - The argument to check
587+
* @returns {boolean} True if argument is an array of [x, y] coordinate pairs
588+
*/
589+
export const isVertices = (arg) => {
590+
if (!Array.isArray(arg) || arg.length < 3) {
591+
return false;
592+
}
593+
594+
for (const vertex of arg) {
595+
if (
596+
!Array.isArray(vertex) ||
597+
vertex.length !== 2 ||
598+
typeof vertex[0] !== 'number' ||
599+
typeof vertex[1] !== 'number'
600+
) {
601+
return false;
602+
}
603+
}
604+
605+
return true;
606+
};
607+
608+
/**
609+
* Ensure a list of vertices forms a closed polygon
610+
* @param {Array<[number, number]>} vertices - Array of [x, y] coordinates
611+
* @returns {Array<[number, number]>} Closed polygon (first vertex repeated at end if needed)
612+
*/
613+
export const verticesToPolygon = (vertices) => {
614+
const polygon = [...vertices];
615+
const firstVertex = vertices.at(0);
616+
const lastVertex = vertices.at(-1);
617+
if (firstVertex[0] !== lastVertex[0] || firstVertex[1] !== lastVertex[1]) {
618+
polygon.push(firstVertex);
619+
}
620+
return polygon;
621+
};
622+
584623
export const insertionSort = (array) => {
585624
const end = array.length;
586625
for (let i = 1; i < end; i++) {

0 commit comments

Comments
 (0)