3d-force-graph
3d-force-graph copied to clipboard
Node Grouping
Related to #271
Have you thought about or experimented with grouping nodes somehow? (for 2D or 3D) Something along this capability: https://cytoscape.org/cytoscape.js-compound-drag-and-drop/
You mentioned creating another node shaped as a box or cube and using that box collision force, but I'm not sure how to make the box expand to include nodes.
Seems like a pretty difficult implementation, but its a capability I think a graph would really benefit from having.
Also something like this would be great too, this is D3 based so it may be easier. http://bl.ocks.org/GerHobbelt/3071239
Hi @Alexithemia, thanks for these suggestions!
The multiple selection and dragging of nodes is possible to implement using the current event props. I've added an illustrating example here: https://vasturiano.github.io/3d-force-graph/example/multi-selection/ https://github.com/vasturiano/3d-force-graph/blob/master/example/multi-selection/index.html
Use any modifier key (shift/ctr/alt) during click to select multiple nodes. The tandem drag is done by applying the same dragging translation intervals to the multiple nodes.
Regarding the D3 example, iirc that is based off of the d3-force-cluster plugin.
I think it would be doable to have such a force in the 3D ecosystem. The first step would be to convert that force plugin to 3 dimensions, so it can be used with d3-force-3d
.
And as for styling a container node that encompasses all nodes in a group, you'd have to compute the 3D convex hull geometry of the group.
Thanks, I think the Hull was the biggest hurdle for me. I'll look into that and see about converting force cluster to 3D and adding it to d3-force-3d.
@vasturiano How do I go about only applying forces to certain nodes? .d3Force only seems to allow me to name a force and provide a force value. It doesn't seem to iterate through all of the nodes or graph objects in that method. It iterates through them in the force itself, but when I have it apply the force differently per node it does not seem to reflect the difference at all.
I do not think force-cluster will be needed or be converted to 3D, as it just gives an attract point to each node with a provided x and y value. This can be recreated and used in 3D by using forceX, forceY and forceZ instead, but I cannot get these to apply differently to specific nodes.
@Alexithemia when creating the force you can set node accessor functions on the force parameters, which will allow you to set different coordinates and/or strengths according to the node, or cluster of nodes. Something like:
myGraph.d3Force('x',
d3.forceX()
.x(node => /* your code */)
.strength(node => /* your code */)
)
https://github.com/vasturiano/d3-force-3d#x_x
@vasturiano Thanks, and for the hull, to add it to the graph it would have to be node with the hull as its three object?
Either that (if you have node grouping in your data) or you could extend the scene to add 3D Hull objects per cluster, updated at every frame.
There's an example of extending the scene here: https://github.com/vasturiano/3d-force-graph/blob/master/example/scene/index.html
@vasturiano I see, which would you think would be better performance wise? I am also wanting the hulls to update every frame of course like you said, so the shape changes to continue encapsulating the nodes if they move, not sure how I would achieve that with either method, sorry I'm getting a bit out of my ability scope at this point.
I would suggest to go with the approach of extending the scene, otherwise you would need to have artificial nodes just for the purpose of drawing the nodes, and then they would also affect the graph layout.
I believe in the example you sent earlier, that is also how it's done. At every graph tick there is an additional layer that updates the hull visually.
Alright, that should work. I don't see an example for your 2D version on extending the scene or a method to access it. Is it done the same way?
@vasturiano I'm also having an issue with the x, y, and z forces. I am applying the forces for nodes to the specific groups when they have them, and pulling them to center when they are not grouped. I'm trying to do this with my code below, but the issue is that the grouped nodes are also pulled towards center as well.
graph
.d3Force('x', d3Force.forceX()
.x(node => node.groupNode ? node.groupNode.x : 0)
.strength(node => node.groupNode ? 1 : .1))
.d3Force('y', d3Force.forceY()
.y(node => node.groupNode ? node.groupNode.y : 0)
.strength(node => node.groupNode ? 1 : .1))
.d3Force('z', d3Force.forceZ()
.z(node => node.groupNode ? node.groupNode.z : 0)
.strength(node => node.groupNode ? 1 : .1))
.d3ReheatSimulation();
Shouldn't this overwrite the previous force a node may have with that given name?
From testing, it seems even when giving the non grouped nodes a strength of zero, and the grouped nodes a point besides center it pulls them to the new point as well as zero. Could there be a bug in the force giving pull to zero as well and the point given?
In this example .strength(node => node.groupNode ? 1 : 0))
is used so there is no center force, only the grouping force gets strength.
the blue and green nodes are already grouped (and for some reason pulled to center) the teal grey and yellow node are not grouped and have no center forces. The teal nodes are then grouped, so they pull together, but then migrate towards center?
@Alexithemia I would try to start with a clean slate and cancel all the default forces in the system. Then add one by one to find the one responsible for the effect you're describing.
You can cancel all the 3 default forces like this:
myGraph
.d3Force('charge', null)
.d3Force('link', null)
.d3Force('center', null);
I'll look through that, and my fault for commenting multiple times but you missed my other question, is there a method for extending the scene in the same with for the 2D version?
Sorry, missed the question indeed.
In 2D there isn't really the concept of 'scene' it's just a canvas that is repainted at every frame. For that I would recommend adding to the nodeCanvasObject()
, possibly with nodeCanvasObjectMode
set to 'before'
, so it doesn't interfere with the current node paints.
That function is called once for every node at every frame, but you can use it to only do the paint once per frame (on the first node for example) by adding a check: if (node === nodes[0]) { /* paint the convex hulls */ }
.
I noticed while messing around with this that the nodeCanvasObject() is still being repeatedly called when the element the canvas has been attached to has disappeared. Then repeatedly stacked each time you load up the graph (my project is in angular so it's using routes to load components instead of new pages) Is there any way to delete the canvas or stop it from calling these functions once it is removed?
@Alexithemia on component unmount you can call ._destructor()
, which basically stops the animation cycle and empties the data.
https://github.com/vasturiano/3d-force-graph/blob/2cc916dfa5eeffd9097e2fa905cb3c89c959394d/src/3d-force-graph.js#L165-L168
Thanks, works great. Sorry to stack up the questions here, but they are all related to this functionality.
This is probably going to be the last one, got most of this working right.
Is there a simple way to add click events to the hulls? I know the convexGeometry / convexHull in three will have some sort of recognition already right? Not sure exactly how to link into that.
For the 2D version i'm drawing a polygon from d3.polygonHull with context.lineTo(points) and context.fill(), would I just need to check every hull during each background click and check if that point is in each one with d3.polygonContains(polygon, point)?
For 2D indeed you could without too much work just reverse the clicked screen coordinates into graph coordinates using screen2GraphCoords(x, y)
, and then check if it intersects with one of the hulls.
3D is more complicated, because screen2GraphCoords(x, y)
doesn't exist. The intersection of a screen pixel with a 3D graph is an infinite line, not a single point. So, unless you have already your hulls registered as custom nodes (in which case you could just take advantage of the onNodeClick()
functionality), you would have to trigger some raycasters on the scene to determine the intersection with your hull objects.
Or, another way is to convert all of your 3D hull boundaries onto the screen domain using graph2ScreenCoords
, and do the comparison on that domain.
On another note, I would love to see the final result of this project, if you end up putting it all together. 😃
I'll definitely get you something to see with all of this.
For the nodes in 3D how does the click recognition work for those? Is there any way I could incorporate the same method?
It's using the raycaster method, under the hood: https://github.com/vasturiano/three-render-objects/blob/04719be2192d639aeda6ffaf498a5659c9509e1a/src/three-render-objects.js#L96-L116
This is another approach (I don't know if it has better performance): I have used a second scene that renders the visible objects with a unique color (from 0x000001 to 0xffffff, 0x000000 is for the background). Then when a click happens you just need to check the value of that pixel.
I don't have the code right now, but you can get an idea of how to retrieve one pixel from a canvas here.
You can put in an array a reference to the object and retrieve it with pixelColor ? arr[pixelColor - 1] : background
Okay I lied. One more issue, and it may be more of a THREE problem. Also showing a bit of the progress so far.
The issue I'm having is at the end of the GIF, and is probably specific to my use case, as I am creating a small canvas for my nodes, drawing them how I want then
const texture = new THREE.Texture(context.canvas);
texture.needsUpdate = true;
const material = new THREE.SpriteMaterial({ map: texture });
const sprite = new THREE.Sprite(material);
and using the sprite as the node.
But the transparent part of the node makes everything behind it besides other nodes invisible. You can see those top two nodes making part of my polygon transparent after the camera is moved and they are on top of it. This also happens to links as I referenced in #248 and you can see it as well in your example https://vasturiano.github.io/3d-force-graph/example/text-nodes/
I have tried Graph.renderer().context.disable(Graph.renderer().context.DEPTH_TEST)
and sprite.material.depthWrite = false;
this fixes the transparent parts, but only because everything renders on top of nodes even if they are behind it.
@Alexithemia I've seen this happen before and I'm not entirely certain why that's the case. I think it's got something to do with how the SpriteMaterial
is set up.
It's more of a ThreeJs
kind of question so I would to do a minimal reproducing and ask for advice in that repo.
If there's anything about the 3d-force-graph
renderer/scene setup that's causing this I'm more than happy to fix it, so please let me know if that's the case.
I can't notice that in the text nodes example though?
@vasturiano its super small there, and probably better to have them in that use case as being able to see all the links behind the text may make it hard to read.
The blue arrow points to where links behind the node are passing into a transparent part of the sprite/text but are then hidden. Red arrow points to a link on top of the node that is visible in that space because its actually on top.
Ah I see it now.
This particular issue associated with links may be related to setting the renderOrder for links to be last instead of being interleaved with nodes. https://github.com/vasturiano/three-forcegraph/blob/16db3d269502c8388e938962b1445a44e13daa86/src/forcegraph-kapsule.js#L754
This was done to prevent weird line rendering glitches, as debated here: https://discourse.threejs.org/t/line-opacity-rendering-issue-with-transparent-spheres/287/5
I'm not sure it's the same issue as you have with your hulls though, since that is affecting also other nodes, not links.
I understand that, don't want links drawing on top of things randomly, I guess the main issue is why the transparent section is not transparent at all times. I'll check with the people over at THREE about it.
@vasturiano I am having trouble getting the raycasters to work right and not fire multiple functions, I can't seem to add some to clicks as the graph clicks are also happening at the same time, will I have to edit the library code to make use of your raycasters or is there a simple way of adding raycasters and preventing other events from happening that I am missing?
@Alexithemia I don't think having additional raycasters from the same camera would conflict with the existing ones. I don't see why you shouldn't be able to do it from the outside if you want to do so. What's the specific issue you're encountering?
Well I need to detect if I right click on a hull, but if I put a raycaster for that it's going to still fire a background right click or node/link right click if they happen to be there from your raycasters. Or am I misunderstanding how they work or how I implement the raycaster?
The raycasters are not coupled with click events. They're just a mean to find the interception of objects within the line of sight.
As for detecting the clicks, I'm not totally certain but perhaps using .onBackgroundClick
could be a way to trigger that.
Having issues with raycasting correctly there. This is what I'm trying but it's not picking up any intersections unless I make one of the hulls take up the entire screen.
.onBackgroundRightClick(event => {
const raycaster = new THREE.Raycaster();
const mouseClick = new THREE.Vector2();
mouseClick.x = event.clientX;
mouseClick.y = event.clientY;
raycaster.setFromCamera(mouseClick, graph.camera());
console.log(raycaster.intersectObjects(graph.scene().children))
}
Having issues with raycasting correctly there. This is what I'm trying but it's not picking up any intersections unless I make one of the hulls take up the entire screen.
.onBackgroundRightClick(event => { const raycaster = new THREE.Raycaster(); const mouseClick = new THREE.Vector2(); mouseClick.x = event.clientX; mouseClick.y = event.clientY; raycaster.setFromCamera(mouseClick, graph.camera()); console.log(raycaster.intersectObjects(graph.scene().children)) }
The mouseClick needs to follow this formula:
mouseClick.x = (event.clientX / graph.width()) * 2 - 1;
mouseClick.y = -(event.clientY / graph.height()) * 2 + 1;
Not sure if that's the issue or not, but as mentioned in the docs, you probably want to set recursive
to true
.
.intersectObject ( object : Object3D, recursive : Boolean, optionalTarget : Array ) : Array object — The object to check for intersection with the ray. recursive — If true, it also checks all descendants. Otherwise it only checks intersection with the object. Default is false. optionalTarget — (optional) target to set the result. Otherwise a new Array is instantiated. If set, you must clear this array prior to each call (i.e., array.length = 0;).
@vasturiano How would I go about adding dragging to these? maybe not specifically to the hulls since those are calculated, but detecting a mouse down on them with raycaster, and getting a translate value in the graph on moving with the mouse down that I can use to move all the nodes inside the group similar to the on node drag?
@Alexithemia in 3d-force-graph
I use DragControls which handles much of the interaction complexity of the drag.
Overall I think the way that I would try to approach this is to make the hulls node objects in the graph, rendered customly using nodeThreeObject
, and manipulating their positions via f*
attributes.
Might be a shorter path into getting it integrated compared to the other approach of drawing outside the framework. It takes some experimentation to find out the easier way.
@vasturiano Is there anyway to access and pop-up the labels that show on node hover somewhere else? I want to make them appear when hovering over the group's hull and show the group name.
@Alexithemia the tooltip is an internal object meant only for interaction of objects known by the chart.
One way for you to get there is to register the hull objects as graph nodes, as mentioned earlier. This would ensure the tooltip functionality would be available to those objects.
Short of that, your other option is to develop your own tooltips outside of the chart. If you'd like to emulate the tooltip behaviour of 3d-force-graph
you can look here: https://github.com/vasturiano/three-render-objects/blob/master/src/three-render-objects.js#L249-L282
I've learnt a lot from reading this issue thread, thanks!
@Alexithemia -- is your project available to look at anywhere? I'd love to learn more from it.