Skip to content

Commit c73ba2b

Browse files
committed
Add filter functions
1 parent ff71771 commit c73ba2b

5 files changed

Lines changed: 344 additions & 61 deletions

File tree

.eslintrc

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@
2323
"no-console": ["error", { "allow": ["warn", "error"] }],
2424
"no-plusplus": 0,
2525
"no-param-reassign": ["error", { "props": false }],
26-
"prefer-destructuring": 0
26+
"prefer-destructuring": 0,
27+
"no-continue": 0,
28+
"prefer-spread": 0
2729
},
2830
"settings": {
2931
"react": {

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
## v1.6.0
2+
3+
- Add the ability to filter down points via `scatterplot.filter(pointIdxs)`. This can be useful if you need to temporarily need to hide points without having to re-instantiate the regl-scatterplot instance. E.g., when calling `scatterplot.filter([0, 1, 2])`, only the first, second, and third point will remain visible. All other points (and their related point connections) will be visually and interactively hidden.
4+
5+
To reset the filter call `scatterplot.unfilter()` or `scatterplot.filter([])`.
6+
17
## v1.5.1
28

39
- Refactor lasso manager to support SSR ([#101](https://github.com/flekschas/regl-scatterplot/issues/101))

src/index.js

Lines changed: 206 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,8 @@ const createScatterplot = (
283283
let selection = [];
284284
const selectionSet = new Set();
285285
const selectionConnecionSet = new Set();
286+
let filteredPoints = [];
287+
const filteredPointSet = new Set();
286288
let numPoints = 0;
287289
let numPointsInView = 0;
288290
let lassoActive = false;
@@ -515,14 +517,27 @@ const createScatterplot = (
515517
return computedPointSizeMouseDetection * pointScale * pxNdc * 0.66;
516518
};
517519

520+
const getPoints = () => {
521+
if (filteredPointSet.size > 0)
522+
return searchIndex.points.filter((_, i) => filteredPointSet.has(i));
523+
return searchIndex.points;
524+
};
525+
526+
const getPointsInBBox = (x0, y0, x1, y1) => {
527+
const pointsInBBox = searchIndex.range(x0, y0, x1, y1);
528+
if (filteredPointSet.size > 0)
529+
return pointsInBBox.filter((i) => filteredPointSet.has(i));
530+
return pointsInBBox;
531+
};
532+
518533
const raycast = () => {
519534
const [xGl, yGl] = getMouseGlPos();
520535
const [xNdc, yNdc] = getScatterGlPos(xGl, yGl);
521536

522537
const pointSizeNdc = getPointSizeNdc();
523538

524539
// Get all points within a close range
525-
const pointsInBBox = searchIndex.range(
540+
const pointsInBBox = getPointsInBBox(
526541
xNdc - pointSizeNdc,
527542
yNdc - pointSizeNdc,
528543
xNdc + pointSizeNdc,
@@ -556,7 +571,7 @@ const createScatterplot = (
556571
// get the bounding box of the lasso selection...
557572
const bBox = getBBox(lassoPolygon);
558573
// ...to efficiently preselect potentially selected points
559-
const pointsInBBox = searchIndex.range(...bBox);
574+
const pointsInBBox = getPointsInBBox(...bBox);
560575
// next we test each point in the bounding box if it is in the polygon too
561576
const pointsInPolygon = [];
562577
pointsInBBox.forEach((pointIdx) => {
@@ -638,6 +653,9 @@ const createScatterplot = (
638653
Math.floor(index / stateTexRes) / stateTexRes + stateTexEps,
639654
];
640655

656+
const isPointFilteredOut = (pointIdx) =>
657+
filteredPointSet.size > 0 && !filteredPointSet.has(pointIdx);
658+
641659
const deselect = ({ preventEvent = false } = {}) => {
642660
if (lassoClearEvent === LASSO_CLEAR_ON_DESELECT) lassoClear();
643661
if (selection.length) {
@@ -675,15 +693,21 @@ const createScatterplot = (
675693
for (let i = selection.length - 1; i >= 0; i--) {
676694
const pointIdx = selection[i];
677695

678-
if (pointIdx < 0 || pointIdx >= numPoints) {
696+
if (
697+
pointIdx < 0 ||
698+
pointIdx >= numPoints ||
699+
isPointFilteredOut(pointIdx)
700+
) {
679701
// Remove invalid selection
680702
selection.splice(i, 1);
681-
} else {
682-
selectionSet.add(pointIdx);
683-
const texCoords = indexToStateTexCoord(pointIdx);
684-
selectionBuffer.push(texCoords[0]);
685-
selectionBuffer.push(texCoords[1]);
703+
continue;
686704
}
705+
706+
selectionSet.add(pointIdx);
707+
selectionBuffer.push.apply(
708+
selectionBuffer,
709+
indexToStateTexCoord(pointIdx)
710+
);
687711
}
688712

689713
selectedPointsIndexBuffer({
@@ -699,6 +723,51 @@ const createScatterplot = (
699723
draw = true;
700724
};
701725

726+
/**
727+
* @param {number | number[]} point
728+
* @param {import('./types').ScatterplotMethodOptions['hover']} options
729+
*/
730+
const hover = (
731+
point,
732+
{ showReticleOnce = false, preventEvent = false } = {}
733+
) => {
734+
let needsRedraw = false;
735+
736+
if (point >= 0 && point < numPoints) {
737+
needsRedraw = true;
738+
const oldHoveredPoint = hoveredPoint;
739+
const newHoveredPoint = point !== hoveredPoint;
740+
if (
741+
+oldHoveredPoint >= 0 &&
742+
newHoveredPoint &&
743+
!selectionSet.has(oldHoveredPoint)
744+
) {
745+
setPointConnectionColorState([oldHoveredPoint], 0);
746+
}
747+
hoveredPoint = point;
748+
hoveredPointIndexBuffer.subdata(indexToStateTexCoord(point));
749+
if (!selectionSet.has(point)) setPointConnectionColorState([point], 2);
750+
if (newHoveredPoint && !preventEvent)
751+
pubSub.publish('pointover', hoveredPoint);
752+
} else {
753+
needsRedraw = +hoveredPoint >= 0;
754+
if (needsRedraw) {
755+
if (!selectionSet.has(hoveredPoint)) {
756+
setPointConnectionColorState([hoveredPoint], 0);
757+
}
758+
if (!preventEvent) {
759+
pubSub.publish('pointout', hoveredPoint);
760+
}
761+
}
762+
hoveredPoint = undefined;
763+
}
764+
765+
if (needsRedraw) {
766+
draw = true;
767+
drawReticleOnce = showReticleOnce;
768+
}
769+
};
770+
702771
const getRelativeMousePosition = (event) => {
703772
const rect = canvas.getBoundingClientRect();
704773

@@ -908,11 +977,21 @@ const createScatterplot = (
908977
rgba[i * 4] = pointSize[i] || 0;
909978
rgba[i * 4 + 1] = Math.min(1, opacity[i] || 0);
910979

911-
const active = Number((pointColorActive[i] || pointColorActive[0])[3]);
912-
rgba[i * 4 + 2] = Math.min(1, Number.isNaN(active) ? 1 : active);
980+
const activeOpacity = Number(
981+
(pointColorActive[i] || pointColorActive[0])[3]
982+
);
983+
rgba[i * 4 + 2] = Math.min(
984+
1,
985+
Number.isNaN(activeOpacity) ? 1 : activeOpacity
986+
);
913987

914-
const hover = Number((pointColorHover[i] || pointColorHover[0])[3]);
915-
rgba[i * 4 + 3] = Math.min(1, Number.isNaN(hover) ? 1 : hover);
988+
const hoverOpacity = Number(
989+
(pointColorHover[i] || pointColorHover[0])[3]
990+
);
991+
rgba[i * 4 + 3] = Math.min(
992+
1,
993+
Number.isNaN(hoverOpacity) ? 1 : hoverOpacity
994+
);
916995
}
917996

918997
return renderer.regl.texture({
@@ -1251,7 +1330,8 @@ const createScatterplot = (
12511330

12521331
return max(minPointScale, camera.scaling[0]) * window.devicePixelRatio;
12531332
};
1254-
const getNormalNumPoints = () => numPoints;
1333+
const getNormalNumPoints = () =>
1334+
filteredPoints && filteredPoints.length ? filteredPoints.length : numPoints;
12551335
const getSelectedNumPoints = () => selection.length;
12561336
const getPointOpacityMaxBase = () =>
12571337
getSelectedNumPoints() > 0 ? opacityInactiveMax : 1;
@@ -1865,8 +1945,113 @@ const createScatterplot = (
18651945
}
18661946
});
18671947

1948+
/**
1949+
* Reset the point filter
1950+
* @param {import('./types').ScatterplotMethodOptions['filter']}
1951+
*/
1952+
const unfilter = ({ preventEvent = false } = {}) => {
1953+
filteredPoints = [];
1954+
filteredPointSet.clear();
1955+
normalPointsIndexBuffer.subdata(createPointIndex(numPoints));
1956+
1957+
return new Promise((resolve) => {
1958+
const finish = () => {
1959+
pubSub.subscribe(
1960+
'draw',
1961+
() => {
1962+
if (!preventEvent) pubSub.publish('unfilter');
1963+
resolve();
1964+
},
1965+
1
1966+
);
1967+
draw = true;
1968+
};
1969+
1970+
// Update point connections
1971+
if (showPointConnections || hasPointConnections(searchIndex.points[0])) {
1972+
setPointConnections(getPoints()).then(() => {
1973+
if (!preventEvent) pubSub.publish('pointConnectionsDraw');
1974+
finish();
1975+
});
1976+
} else {
1977+
finish();
1978+
}
1979+
});
1980+
};
1981+
1982+
/**
1983+
* Filter down to a set of points
1984+
* @param {number | number[]} pointIdxs
1985+
* @param {import('./types').ScatterplotMethodOptions['filter']}
1986+
*/
1987+
const filter = (pointIdxs, { preventEvent = false } = {}) => {
1988+
filteredPoints = Array.isArray(pointIdxs) ? pointIdxs : [pointIdxs];
1989+
1990+
if (filteredPoints.length === 0) return unfilter({ preventEvent });
1991+
1992+
filteredPointSet.clear();
1993+
1994+
const filteredPointsBuffer = [];
1995+
const filteredSelection = [];
1996+
1997+
for (let i = filteredPoints.length - 1; i >= 0; i--) {
1998+
const pointIdx = filteredPoints[i];
1999+
2000+
if (pointIdx < 0 || pointIdx >= numPoints) {
2001+
// Remove invalid filtered points
2002+
filteredPoints.splice(i, 1);
2003+
continue;
2004+
}
2005+
2006+
filteredPointSet.add(pointIdx);
2007+
filteredPointsBuffer.push.apply(
2008+
filteredPointsBuffer,
2009+
indexToStateTexCoord(pointIdx)
2010+
);
2011+
2012+
if (selectionSet.has(pointIdx)) filteredSelection.push(pointIdx);
2013+
}
2014+
2015+
// Update the normal points index buffers
2016+
normalPointsIndexBuffer.subdata(filteredPointsBuffer);
2017+
2018+
// Update selection
2019+
select(filteredSelection, { preventEvent });
2020+
2021+
// Unset any potentially hovered point
2022+
hover(-1, { preventEvent });
2023+
2024+
return new Promise((resolve) => {
2025+
const finish = () => {
2026+
pubSub.subscribe(
2027+
'draw',
2028+
() => {
2029+
if (!preventEvent)
2030+
pubSub.publish('filter', { points: filteredPoints });
2031+
resolve();
2032+
},
2033+
1
2034+
);
2035+
draw = true;
2036+
};
2037+
2038+
// Update point connections
2039+
if (showPointConnections || hasPointConnections(searchIndex.points[0])) {
2040+
setPointConnections(getPoints()).then(() => {
2041+
if (!preventEvent) pubSub.publish('pointConnectionsDraw');
2042+
// We have to re-apply the selection because the connections might
2043+
// have changed
2044+
select(filteredSelection, { preventEvent });
2045+
finish();
2046+
});
2047+
} else {
2048+
finish();
2049+
}
2050+
});
2051+
};
2052+
18682053
const getPointsInView = () =>
1869-
searchIndex.range(
2054+
getPointsInBBox(
18702055
bottomLeftNdc[0],
18712056
bottomLeftNdc[1],
18722057
topRightNdc[0],
@@ -2049,6 +2234,10 @@ const createScatterplot = (
20492234
return;
20502235
}
20512236

2237+
// Reset filter
2238+
filteredPoints = [];
2239+
filteredPointSet.clear();
2240+
20522241
let pointsCached = false;
20532242
if (points) {
20542243
if (options.transition) {
@@ -2472,7 +2661,7 @@ const createScatterplot = (
24722661
showPointConnections = !!newShowPointConnections;
24732662
if (showPointConnections) {
24742663
if (hasPointConnections(searchIndex.points[0])) {
2475-
setPointConnections(searchIndex.points).then(() => {
2664+
setPointConnections(getPoints()).then(() => {
24762665
pubSub.publish('pointConnectionsDraw');
24772666
draw = true;
24782667
});
@@ -2959,51 +3148,6 @@ const createScatterplot = (
29593148
preventEventView = preventEvent;
29603149
};
29613150

2962-
/**
2963-
* @param {number | number[]} point
2964-
* @param {import('./types').ScatterplotMethodOptions['hover']} options
2965-
*/
2966-
const hover = (
2967-
point,
2968-
{ showReticleOnce = false, preventEvent = false } = {}
2969-
) => {
2970-
let needsRedraw = false;
2971-
2972-
if (point >= 0 && point < numPoints) {
2973-
needsRedraw = true;
2974-
const oldHoveredPoint = hoveredPoint;
2975-
const newHoveredPoint = point !== hoveredPoint;
2976-
if (
2977-
+oldHoveredPoint >= 0 &&
2978-
newHoveredPoint &&
2979-
!selectionSet.has(oldHoveredPoint)
2980-
) {
2981-
setPointConnectionColorState([oldHoveredPoint], 0);
2982-
}
2983-
hoveredPoint = point;
2984-
hoveredPointIndexBuffer.subdata(indexToStateTexCoord(point));
2985-
if (!selectionSet.has(point)) setPointConnectionColorState([point], 2);
2986-
if (newHoveredPoint && !preventEvent)
2987-
pubSub.publish('pointover', hoveredPoint);
2988-
} else {
2989-
needsRedraw = +hoveredPoint >= 0;
2990-
if (needsRedraw) {
2991-
if (!selectionSet.has(hoveredPoint)) {
2992-
setPointConnectionColorState([hoveredPoint], 0);
2993-
}
2994-
if (!preventEvent) {
2995-
pubSub.publish('pointout', hoveredPoint);
2996-
}
2997-
}
2998-
hoveredPoint = undefined;
2999-
}
3000-
3001-
if (needsRedraw) {
3002-
draw = true;
3003-
drawReticleOnce = showReticleOnce;
3004-
}
3005-
};
3006-
30073151
const initCamera = () => {
30083152
if (!camera)
30093153
camera = createDom2dCamera(canvas, { isPanInverted: [false, true] });
@@ -3305,6 +3449,7 @@ const createScatterplot = (
33053449
deselect,
33063450
destroy,
33073451
draw: publicDraw,
3452+
filter,
33083453
get,
33093454
hover,
33103455
redraw,
@@ -3314,6 +3459,7 @@ const createScatterplot = (
33143459
set,
33153460
export: exportFn,
33163461
subscribe: pubSub.subscribe,
3462+
unfilter,
33173463
unsubscribe: pubSub.unsubscribe,
33183464
view,
33193465
zoomToLocation,

0 commit comments

Comments
 (0)