Search code examples
asynchronousgraphcallbackpromisevivagraphjs

Asynchronous update of nodes properties after the graph is rendered (Vivagraph.js)


I am trying to populate nodes' data in a graph, asynchronously.

How to ensure that data fetched asyncrosly is actually bound to the graph, and rendered when ready?

First, you render the graph structure, node and links. Second, you render data as nodes' properties, when data is ready. New node can by dynamically added by interacting on parents' nodes, and don't want to wait for completion of node's properties.

Please note I am using Vivagraph.js library.graph is an object created with the library, addLinks() and getNode() are its function properties - see the demo at Vivagraph demo, I am using that as a draft of my attempts.

The issue I experience is that nodes are rendered in the graph as soon as they are added - addNode() either addLinks(node1, node2) functions -, while node's properties fetched asynchronously - getNode(node).property = updatedValue - result undefined.

EDITED - Simplified code based on comment

Below I include a working mockup version, based on tutorial provided by @Anvaka, the author of this (awesome) library.

My goal is to render the graph immediately, enabling interaction, and update data while it is being fetched from third parties.

// attempt 1: fetch data async
var fetchInfo = function (graph, nodeId) {
    var root = 'http://jsonplaceholder.typicode.com';

    $.ajax({
        url: root + '/photos/' + nodeId,
        method: 'GET'
    }).then(function (data) {
        graph.getNode(nodeId).data = data.thumbnailUrl;
        console.log(graph.getNode(nodeId));
    });
};

// attempt 2: defer ajax directly
var fetchInfo_2 = function (graph, nodeId) {
    var root = 'http://jsonplaceholder.typicode.com';

    return $.ajax({
        url: root + '/photos/' + nodeId,
        method: 'GET'
    });
};

function main() {
    // As in previous steps, we create a basic structure of a graph:
    var graph = Viva.Graph.graph();

    graph.addLink(1, 2);
    fetchInfo(graph, 1); // updated data is undefined when graph is rendered 
    fetchInfo(graph, 2); // updated data is undefined when graph is rendered 

    /* trying a different outcome by deferring whole ajax
    graph.getNode(1).data = fetchInfo_2(1).done(function(data) {
      data.thumbnailUrl;
    }); // the whole object is deferred but cannot fetch data

    graph.getNode(2).data = fetchInfo_2(2).done(function(data) {
      data.thumbnailUrl;
    });  // the whole object is deferred but cannot fetch data
    */


    var graphics = Viva.Graph.View.svgGraphics(),
        nodeSize = 24,
        addRelatedNodes = function (nodeId, isOn) {
            for (var i = 0; i < 6; ++i) {
                var child = Math.floor((Math.random() * 150) + nodeId);
                // I add children and update data from external sources
                graph.addLink(nodeId, child);
                fetchInfo(graph, child);
            }
        };


    // dynamically add nodes on mouse interaction
    graphics.node(function (node) {
        var ui = Viva.Graph.svg('image')
            .attr('width', nodeSize)
            .attr('height', nodeSize)
            .link(node.data);

        console.log('rendered', node.id, node.data);

        $(ui).hover(function () {

            // nodes are rendered; nodes' data is undefined 
            addRelatedNodes(node.id);
        });
        return ui;
    }).placeNode(function (nodeUI, pos) {
        nodeUI.attr('x', pos.x - nodeSize / 2).attr('y', pos.y - nodeSize / 2);
    });

    graphics.link(function (link) {
        return Viva.Graph.svg('path')
            .attr('stroke', 'gray');
    }).placeLink(function (linkUI, fromPos, toPos) {
        var data = 'M' + fromPos.x + ',' + fromPos.y +
            'L' + toPos.x + ',' + toPos.y;

        linkUI.attr("d", data);
    })

    var renderer = Viva.Graph.View.renderer(graph, {
        graphics: graphics
    });
    renderer.run();
}

main();
svg {
    width: 100%;
    height: 100%;
}
<script src="https://rawgit.com/anvaka/VivaGraphJS/master/dist/vivagraph.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script>


Solution

  • As I said in the comments, any work you want to do as a result of an asynchronous process must be done in the callback. This includes any rendering work.

    For jQuery's deferreds (like the result of an Ajax call), the callback is defined through .then or .done, enabling you to detach the act of fetching a resource from working with that resource.

    Setting the image source is rendering work, so you must do it in the callback. Below is a function that fetches images and returns the deferred result and the node callback function uses that to do its own piece of work.

    function fetchInfo(id) {
        var root = 'http://jsonplaceholder.typicode.com';
        return $.getJSON(root + '/photos/' + id);
    }
    
    function main() {
        var graph = Viva.Graph.graph(),
            graphics = Viva.Graph.View.svgGraphics(),
            nodeSize = 24,
            addRelatedNodes = function (nodeId, count) {
                var childId, i;
                for (i = 0; i < count; ++i) {
                    childId = Math.floor(Math.random() * 150) + nodeId;
                    graph.addLink(nodeId, childId);
                }
            };
        
        graphics
            .node(function (node) {
                var ui = Viva.Graph.svg('image')
                    .attr('width', nodeSize)
                    .attr('height', nodeSize)
                    .link('http://www.ajaxload.info/images/exemples/24.gif');
                
                $(ui).dblclick(function () {
                    addRelatedNodes(node.id, 6);
                });
    
                // render when ready
                fetchInfo(node.id).done(function (data) {
                    ui.link(data.thumbnailUrl);
                });
    
                return ui;
            })
            .placeNode(function (nodeUI, pos) {
                nodeUI.attr('x', pos.x - nodeSize / 2).attr('y', pos.y - nodeSize / 2);
            })
            .link(function (link) {
                return Viva.Graph.svg('path')
                    .attr('stroke', 'gray');
            })
            .placeLink(function (linkUI, fromPos, toPos) {
                var data = 
                    'M' + fromPos.x + ',' + fromPos.y +
                    'L' + toPos.x + ',' + toPos.y;
    
                linkUI.attr("d", data);
            });
    
        graph.addLink(1, 2);
    
        Viva.Graph.View.renderer(graph, {
            graphics: graphics
        }).run();
    }
    
    main();
    svg {
        width: 100%;
        height: 100%;
    }
    img {
        cursor: pointer;
    }
    <script src="https://rawgit.com/anvaka/VivaGraphJS/master/dist/vivagraph.js"></script>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script>

    Sadly, it does not seem to run in the StackSnippets, but it works over on jsFiddle: http://jsfiddle.net/tL9992ua/