force-graph
force-graph copied to clipboard
Drag select (box)
Is your feature request related to a problem? Please describe. I see you have a getGraphBbox method, that will give you dimensions of a box containing all the nodes. I was hoping to get functionality to create a box and it would return all the nodes inside that box.
Describe the solution you'd like Add option to turn on drag(box) selection, shift+click and drag to create a box (just like you do on your desktop with applications) and it would return all nodes inside that box to a callback function.
Additional context I also use your 3D version and would be awesome to have it there too, but I know how much more difficult it would be.
@Alexithemia I think you could build something like that on top of the force-graph
component.
Most of the pieces should be in place:
- You could render an additional
div
element on top of the existing canvas, to catch all the pointer drag interactions and potentially draw a visual selection box. - Once you have a bounding box, you can translate that to the graph coordinates domain using the
screen2GraphCoords
method. Then, you can easily find out which nodes are included in your box by simply running an intersection check for the nodes coordinates (x
andy
attributes). - From here on, the dragging functionality should be no different than the multiple node selection example.
I think this functionality is much too specific to support it inside the component, but having an example with this implementation could be useful to other developers.
As for 3D, the implementation would probably be quite similar, but step 2 is indeed more difficult because a screen box translates to not a plane polygon but a frustum, so the translation and intersection math is a bit more involved.
I am not able to create an event listener for mousedown on the canvas or the container element of the graph, something seems to be blocking it. Do you know how to get that to go through? Or how would you get a div box to render? I want it to work when I shift+click+drag from anywhere (background or top of nodes).
It does work with 3D though.
I also need to be able to prevent dragging the graph or a node if shift is pressed during mousedown and mousedrag so I can create a box, but stopPropagation does not stop those from occurring on 3D currently.
@Alexithemia the propagation of those pointer events is stopped by d3-zoom, as you can see from the documentation. d3-zoom
is what handles the panning and zoom interactions on the graph.
However, you can still catch those events if you place a div
element on top of the graph canvas (via absolute positioning). That's what I meant by point 1 previously.
You can also use this "event trapping" div to draw your drag box and potentially block the propagation of pointer drag events to the underlying graph canvas, when desired.
I am probably missing something, but when I place a div over the graph it doesn't let any click events though it. There is a way to let the events through, by using pointerEvents: none
in css. But then I can't use it to capture any events to make the box.
Hi @Alexithemia. I was thinking that you actually wanted to handle all the pointer events in your overlay, and not let any through to the graph. You can instead just use the overlay for drawing (with pointer-events: none
), and capture the pointer events for click drag etc in your graph DOM element.
As mentioned previously d3-zoom
blocks the propagation of some of these events. However, there's a few ways around it:
- You can use pointer events instead of mouse events (used by
d3-zoom
) as the former have precedence over the latter. SoaddEventListener('pointerdown', ...)
instead ofaddEventListener('mousedown', ...)
. Those should be triggered in your DOM element. You can even chose to propagate of cancel default conditionally to allow some events through to the graph. - If you use the capture phase of the
mousedown
event you should be able to intercept it before it reaches the graph child element (and enters the bubble up phase). Again you can control whether to propagate them further or not. Something like:addEventListener('mousedown', myHandler, { useCapture: true })
.
Here is my code I used for 2D box select.
// forceGraph element is the element provided to the Force Graph Library
document.getElementById('forceGraph').addEventListener('pointerdown', (e) => {
if (e.shiftKey) {
e.preventDefault();
boxSelect = document.createElement('div');
boxSelect.id = 'boxSelect';
boxSelect.style.left = e.offsetX.toString() + 'px';
boxSelect.style.top = e.offsetY.toString() + 'px';
boxSelectStart = {
x: e.offsetX,
y: e.offsetY
};
// app element is the element just above the forceGraph element.
document.getElementById('app').appendChild(this.boxSelect);
}
});
document.getElementById('forceGraph').addEventListener('pointermove', (e) => {
if (e.shiftKey && boxSelect) {
e.preventDefault();
if (e.offsetX < boxSelectStart.x) {
boxSelect.style.left = e.offsetX.toString() + 'px';
boxSelect.style.width = (boxSelectStart.x - e.offsetX).toString() + 'px';
} else {
boxSelect.style.left = boxSelectStart.x.toString() + 'px';
boxSelect.style.width = (e.offsetX - boxSelectStart.x).toString() + 'px';
}
if (e.offsetY < boxSelectStart.y) {
boxSelect.style.top = e.offsetY.toString() + 'px';
boxSelect.style.height = (boxSelectStart.y - e.offsetY).toString() + 'px';
} else {
boxSelect.style.top = boxSelectStart.y.toString() + 'px';
boxSelect.style.height = (e.offsetY - boxSelectStart.y).toString() + 'px';
}
} else if (boxSelect) {
boxSelect.remove();
}
});
document.getElementById('forceGraph').addEventListener('pointerup', (e) => {
if (e.shiftKey && boxSelect) {
e.preventDefault();
let left, bottom, top, right;
if (e.offsetX < boxSelectStart.x) {
left = e.offsetX;
right = boxSelectStart.x;
} else {
left = boxSelectStart.x;
right = e.offsetX;
}
if (e.offsetY < boxSelectStart.y) {
top = e.offsetY;
bottom = boxSelectStart.y;
} else {
top = boxSelectStart.y;
bottom = e.offsetY;
}
runBoxSelect(left, bottom, top, right);
boxSelect.remove();
} else if (boxSelect) {
boxSelect.remove();
}
});
const runBoxSelect = (left, bottom, top, right) => {
const tl = canvas.screen2GraphCoords(left, top);
const br = canvas.screen2GraphCoords(right, bottom);
const hitNodes = [];
getNodesFromState(store).forEach(node => {
if (tl.x < node.x && node.x < br.x && br.y > node.y && node.y > tl.y) {
hitNodes.push(node);
};
});
// run code to select your nodes here
return selectGraphObjects(hitNodes);
}
CSS for box to show up on top -
#boxSelect {
position: absolute;
z-index: 300000;
border-style: dotted;
border-color: #3e74cc;
background-color: rgba(255, 255, 255, 0.5);
pointer-events: none;
}