0

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>
Tomalak
  • 332,285
  • 67
  • 532
  • 628
user305883
  • 1,635
  • 2
  • 24
  • 48
  • I appreciate that you try to put as much information into your question as you can, but you are overwhelming the reader while, ironically, at the same time you do not supply enough information. Which one of your attempts are we supposed to fix? All of them? Not gonna happen. The first line in your first code sample is a syntax error right there. Please provide at least syntactically correct code, code that actually runs when copied-and-pasted. What's `children`? What's `graph`? You never tell. Please create a single (!) [MCVE](http://stackoverflow.com/help/mcve). – Tomalak Aug 03 '15 at 05:24
  • Hi @Tomalak, thank you for the MCVE guidelines. I m asking to *conceptually* guide on how to async update nodes' properties of a graph generated with Vivagraph library. Nodes fetched async are added to the graph, but nodes' properties are not updated. Graph is a vivagraph object (see demo in url); addNode() and getNode() are functions from Viva library. My issue: properties async updated on nodes result undefined when nodes' are rendered. The purpose of my attempts is to explain what I tried to do, rather then fixing code. Is my question clearer? – user305883 Aug 03 '15 at 10:43
  • Well, *conceptually* you do all work that comes out of an asynchoronous process in the callback of that process. That includes any rendering/re-rendering work. As I said, I understand what you are trying to do with your many code samples. A) I'm saying that it is not helpful, to the contrary, it's counter-productive. B) I've asked you to create a single (!) self-contained sample of the current state of your code. – Tomalak Aug 03 '15 at 12:04
  • @Tomalak thank you for your feedback, I followed your suggestion and made a single working simplified sample - hope it is clearer and helpful now! – user305883 Aug 03 '15 at 13:01

1 Answers1

1

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/

Tomalak
  • 332,285
  • 67
  • 532
  • 628
  • Thank you @Tomalak for a solution and also for suggesting how to edit. I need another de-brief. I want to avoid to repeat the query for **each** node. So I fetch a first set of N children with 'name' property. Problem is that when node is placed on the graph as result of graph.addLink(), 'name' is undefined cause. (In your example, if would work if I fetch data *for each new node*). Is it possible to defer and reuse an object more than once? Would it be a good implementation to use an hash table where to store all information? how to use a property only when is updated? – user305883 Aug 04 '15 at 15:08
  • basically what I am trying to do now is something like this. I have an object with name property, which stores values while they are getting fetched from external sources. This object is already rendered. I want to promise the value of a property to a variable which will be rendered. Is it possibile? I am trying something like: `name = new Promise(function(id) { return ht.getItem(id).name //get property 'name' from an item in object HashTable });` but I get: `[[PromiseStatus]]: "rejected", [[PromiseValue]]: TypeError: Cannot read property 'name' of undefined` – user305883 Aug 04 '15 at 15:36
  • Sure, you can store and re-use deferreds as often as you like. If you want to fetch in bulk, just do that and cache the resulting deferreds. Now you can call `.done()` on them as often as you like, for example once per node. As long as the Ajax response is pending, the `.done()` callbacks wait, after the response has arrived, they run immediately. – Tomalak Aug 04 '15 at 15:40
  • Unless you have a dedicated promise library in your page, I would recommend for the sake of compatibility that you don't use `new Promise`. And you don't need to use that anyway, you can simply store the jQuery Ajax requests. – Tomalak Aug 04 '15 at 15:43
  • @user305883 Compare http://jsfiddle.net/tL9992ua/1/, where I have only modified the `fetchInfo()` function, the rest of the code is exactly the same as in the initial version. Note that I've switched to jQuery 1.8+ where this kind of promise chaining through `.then()` was introduced. I take it that you don't insist on using a jQuery version as ancient as 1.7.2. – Tomalak Aug 04 '15 at 16:21
  • ..ok. no promises here :) could you provide an example on how to cache results, based on the example you gave? I tried to store the response in a global variable `tmp` and `.done()` in the rendering part, but `tmp` would be still undefined. I also tried with an Hash Table, but still cumbersome for me how to reuse an object in the rendering part (in another function()).. – user305883 Aug 04 '15 at 17:47
  • Hm, have you actually looked at my code sample from my previous comment? It *does* cache the Ajax result, look at the network tab in the developer tab. No Ajax requests after the first one. – Tomalak Aug 04 '15 at 19:31
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/85145/discussion-between-user305883-and-tomalak). – user305883 Aug 04 '15 at 19:50
  • I saw your comments after having written mine (I let the console opened :) Since the answer is actually answered I moved the discussion to chat. – user305883 Aug 04 '15 at 20:27