vasturiano / 3d-force-graph

3D force-directed graph component using ThreeJS/WebGL
https://vasturiano.github.io/3d-force-graph/example/large-graph/
MIT License
4.67k stars 824 forks source link

Update nodeThreeObject #61

Closed gitgitwhat closed 4 years ago

gitgitwhat commented 6 years ago

I'm struggling with how to get this to work.

I use the .nodeThreeObject to create a THREE.Group of objects for every node added to the scene. I then setInterval and AJAX query to update the status of the node. Based on the received data, I need to update the node and all of the associated THREE objects in the group.

For example, I have a TorusGeometry that I use as a status indicator that is a part of the group. When the update arrives, I'd like to be able to look up the node and if found, update the value that represents the Arc of the TorusGeometry object associated with the node all without having to remove the node and re-adding because that causes the "popping" re-draw of the node. Hope that makes sense.

Here is some code...

const elem = document.getElementById('3d-graph');
Graph = ForceGraph3D()
(elem)
.graphData(_graphdata)
.nodeThreeObject(node => {
    var s = new THREE.SphereGeometry();
    //...
    var t = new THREE.TorusGeometry();
    //...
    var group = new THREE.Group();
    group.add(s);
    group.add(t);
    return group;
})

Then after the AJAX request, I call updateNode on the node but this is where I'm stuck. How can I access objects within the node group (such as the TorusGeometry object) to change and redraw the values?

        function updateNode(n) {
            const { nodes, links } = Graph.graphData();
            var found = 0;
            for (var i = 0; i < nodes.length; i++) {
                if (nodes[i].id == n.id) {
                    found = 1;
                    break;
                } 
            }

            if (found == 1) {
        //WHAT DO I DO HERE TO MAKE THE NODE REFLECT 
                //UPDATE VALUES WITHOUT UPDATING THE WHOLE
                //SCENE WITH Graph.graphData({ nodes, links });
            }
        }
vasturiano commented 6 years ago

@gitgitwhat if you wish to change the appearance of your nodes dynamically, what I'd recommend is re-calling the .nodeThreeObject method when you wish the nodes to reflect the changes.

So, you'd simply do:

Graph.nodeThreeObject(node => /* the new render method */)

This will tell the graph to re-run through each of the nodes and call the above method in turn. It will iterate through the nodes only once every time the above method is invoked.

Let me know if this solves your case.

gitgitwhat commented 6 years ago

So simple. Yes I am seeing the nodes update now. Thanks! One slight problem though is that every call to update causes all the nodes to expand outwards slightly from their current position. You can see an example here.

https://codepen.io/anon/pen/VxKLGe

Only the color is being changed but the position is being updated somewhere during the refresh. This example also highlights the node drag issue that I also reported.

vasturiano commented 6 years ago

Ah yes. The nodes move slightly on update because the force engine is re-ignited every time the graph configuration is modified. I've been thinking of decoupling the update methods between those related to the force layout, and those related to the rendering (such as this), so that modifying visual config would not trigger engine versions, and vice-versa.

artyomtrityak commented 6 years ago

@gitgitwhat you need to store your TorusGeometry somewhere outside (state, this, window etc) and you can get full control over appearance via THREE.js for example

const { x, y, z } = this.toursMesh.scale;
this.toursMesh.scale.set(x + 0.2, y + 0.2, z + 0.2);
gitgitwhat commented 6 years ago

Thanks @artyomtrityak. I got the control I was looking for using the nodeThreeObject method that @vasturiano mentioned. But I'll keep your suggestion in mind as I'm still learning THREE.js.

jlerbsc commented 4 years ago

@vasturiano has this been developed? "I've been thinking of decoupling the update methods between those related to the force layout, and those related to the rendering (such as this), so that modifying visual config would not trigger engine versions, and vice-versa."

vasturiano commented 4 years ago

@jlerbsc yes, that is the way it functions in the current version.

jlerbsc commented 4 years ago

@vasturiano, Nice but how to invoke update methods. Do you talk about linkPositionUpdate, graphData methods?

jonathan81342 commented 4 years ago

What if I just want to delete the current graph and call ForceGraph3D again with a different graph? I have tried deleting the 3d-graph div node child and calling ForceGraph3D with an empty graph, but whatever I do the old graph is still displayed beside the new one. (I hope there is answer that does not require my learning details of THREE...)

vasturiano commented 4 years ago

@jonathan81342 if you remove from the DOM the div element onto which the 3d-force-graph is being rendered, it's not really possible that the graph is still showing, since it's no longer in the document.

In any case, if you wish to replace the existing graph with a new one, you could simply pass a new data set to it, by invoking .graphData(<your new data set). The graph will automatically remove the old nodes/links and add the new ones.

jonathan81342 commented 4 years ago

@vasturiano Thanks for responding! I'm still missing something, however. I tried the "simply...automatic" suggestion as follows (only the relevant parts included now, but I'll attach the whole - only one page - script if you want):

    function getGraph() {
        var hg = document.getElementById("hgin").value;
        hga = JSON.parse(hg);
        gData = jsonize(hga);

            // make graph in 3d-graph
        ForceGraph3D()
                (document.getElementById('3d-graph'))
            .graphData(gData)
            .linkWidth(0)
            .linkOpacity(1);
    }

jsonize converts an array like [[1,2,3],[2,4,5]] into the nodes-links object format. The function getGraph is invoked by an HTML button that reads a text field containing the text version of the array. Doesn't this pass the new graph to ForceGraph3D and hence also to 3d-graph?

But if I do this once with [[1,2,3],[2,4,5]], then press the button again after editing the input field to [[8,9,10]], I get this picture:

Screen Shot 2020-05-25 at 1 28 34 PM

Both graphs are there, though the first one has lost its node balls.

vasturiano commented 4 years ago

@jonathan81342 you shouldn't invoke the instantiation more than once, this part: ForceGraph3D()(document.getElementById('3d-graph')).

Just instantiate it once and keep a reference to the component when switching data sets, something like:

const myForceGraph = ForceGraph3D()
      (document.getElementById('3d-graph'))
      .linkWidth(0)
      .linkOpacity(1);

function getGraph() {
        var hg = document.getElementById("hgin").value;
        hga = JSON.parse(hg);
        gData = jsonize(hga);

                 myForceGraph.graphData(gData);
    }
jonathan81342 commented 4 years ago

@vasturiano That worked, thanks! And generally, thanks for making ForceGraph3D available.

ChickendCode commented 3 years ago

@gitgitwhat if you wish to change the appearance of your nodes dynamically, what I'd recommend is re-calling the .nodeThreeObject method when you wish the nodes to reflect the changes.

So, you'd simply do:

Graph.nodeThreeObject(node => /* the new render method */)

This will tell the graph to re-run through each of the nodes and call the above method in turn. It will iterate through the nodes only once every time the above method is invoked.

Let me know if this solves your case.

For reactjs, how i should be recall nodeThreeObject?

vasturiano commented 3 years ago

@TranDangTin that is more a question for the react-force-graph repo, but essentially you can just use it as a regular prop:

nodeThreeObject={node => ...}
ChickendCode commented 3 years ago

@TranDangTin that is more a question for the react-force-graph repo, but essentially you can just use it as a regular prop:

nodeThreeObject={node => ...}

what i want to ask is when updating data node when clicking node it doesnt call the nodeThreeObject function again, however i found a way to call it again. i use fgref.current.refresh()

Thank you

benny-noumena commented 2 months ago

@vasturiano Hi! First of all may thanks for this awesome library and all the effort you put into it.

I'm still struggling with update a customGeometry on hover. As I understood you would call graph.nodeThreeObject but wouldn't that be an overkill if I would like to change only one instance?