hyper-scatter separates renderer state from input handling.
- The renderer owns view math, hit testing, lasso selection, and emphasis rendering.
- The 2D
createInteractionController()helper wires common DOM input patterns to aRenderer. - 3D input handling is currently host-managed.
import { createInteractionController } from "hyper-scatter";
const controller = createInteractionController(canvas, plot, {
lassoPredicate: (event) => event.shiftKey,
onHover: (hit) => {
console.log(hit?.index ?? null);
},
onLassoUpdate: (_dataPolygon, screenPolygon) => {
plot.setLassoPolygon(screenPolygon);
plot.render();
},
onLassoComplete: (result, _dataPolygon, screenPolygon) => {
plot.setLassoPolygon(screenPolygon);
if (result.kind === "indices" && result.indices) {
plot.setSelection(result.indices);
plot.setInactiveOpacity(result.indices.size > 0 ? 0.35 : 1);
}
plot.render();
},
});Supported controller options:
observeResize: keep renderer size in sync with the canvas usingResizeObserverwheelZoomScale: customize wheel-to-zoom sensitivitylassoPredicate: choose the gesture that enters lasso modelassoMinSampleDistPx: screen-space point sampling threshold while dragginglassoMaxVertsInteraction: polygon simplification budget during draglassoMaxVertsFinal: polygon simplification budget on completiononHover: hover callback after hit testingonLassoUpdate: callback during drag with both data-space and screen-space polygonsonLassoComplete: callback with the finalSelectionResult
Default lasso gesture:
Shift+MetadragShift+Ctrldrag
If you want Shift-drag instead, override lassoPredicate.
The renderer has three emphasis channels:
setSelection(indices)for the primary active subsetsetHighlight(indices)for secondary emphasis such as neighbors or search hitssetHovered(index)for transient pointer feedback
You can dim everything else with setInactiveOpacity(alpha).
plot.setSelection(new Set([12, 18]));
plot.setHighlight(new Set([4, 5, 6, 7]));
plot.setInactiveOpacity(0.3);
plot.render();Customize ring treatment with setInteractionStyle():
plot.setInteractionStyle({
selectionColor: "#f59e0b",
selectionRadiusOffset: 2,
selectionRingWidth: 2,
highlightColor: "#94a3b8",
highlightRadiusOffset: 1,
highlightRingWidth: 1.5,
hoverColor: "#ffffff",
hoverFillColor: null,
hoverRadiusOffset: 3,
});
plot.render();These are separate concerns.
Use setLassoPolygon() while the user is dragging:
plot.setLassoPolygon(screenPolygon, {
strokeColor: "#4f46e5",
strokeWidth: 2,
fillColor: "rgba(79, 70, 229, 0.15)",
});
plot.render();Clear it with:
plot.setLassoPolygon(null);
plot.render();Call lassoSelect(screenPolygon) directly, or use the controller callback.
2D renderers may return:
kind: "indices"with an explicitSet<number>kind: "geometry"with a data-space polygon-backed predicate
If you only need the final count, use countSelection():
const count = await plot.countSelection(result, { yieldEveryMs: 0 });3D renderers always return index-based selections.
For 3D renderers, wire your own pointer events and call the renderer directly:
pan(deltaX, deltaY, modifiers)zoom(anchorX, anchorY, delta, modifiers)hitTest(screenX, screenY)lassoSelect(polyline)
That keeps the public renderer surface aligned across 2D and 3D, even though only 2D ships with a built-in controller today.