Iain Dillingham
Iain Dillingham

Reputation: 625

Checking that multiple asynchronous functions have completed before drawing a d3 chart

Context: I'm attempting to query the CrisisNET API multiple times, to find the number of reports that were filed each day in a range of days.

Question: how do check that all asynchronous functions have completed before using the totals array to draw a d3 chart?

Code:

var format = d3.time.format.iso,
    min = format.parse('2014-06-01'),
    max = format.parse('2014-07-01'),
    range = d3.time.day.utc.range(min, max);

totals = [];

for (var i = 0; i < range.length - 1; i++) {

    var request = (function(before, after) {

        var url = 'http://api.crisis.net/item?'
            + '&before=' + format(before)
            + '&after=' + format(after)
            + '&placeName=Syria'
            + '&limit=0'
            + '&apikey=[apikey]';

        d3.json(url, function(error, data) {

            var total =  {

                'date': after,
                'total': data.total

            };

            totals.push(total);
        });
    }(range[i + 1], range[i]));
}

Thank you in advance for any help. I'm a relative newcomer to JavaScript and am still trying to understand callbacks, asynchronous functions, and the like.

Upvotes: 1

Views: 761

Answers (3)

ckersch
ckersch

Reputation: 7687

If you know how many functions you're expecting to have done, and you have a callback on each one, you can use a wrapper function to count them and execute something else once they've all executed. Something like this:

function asyncCounter(numCalls, callback){
    this.callback = callback;
    this.numCalls = numCalls;
    this.calls = 0;
};

asyncCounter.prototype.increment = function(){
    if(this.calls++ === this.numCalls){
        this.callback();
    }
};

You then create an asyncCounter object and increment it each time you read a JSON:

var myAsyncCounter = new asyncCounter(numCallsExpected, callback);

...//code outside the JSON calls
d3.json(url, function(error, data) {
    ... //code you want to execute before incrementing the counter
    myAsyncCounter.increment();
}

callback will execute after all of the code in the d3.json calls has finished.

Upvotes: 1

jshanley
jshanley

Reputation: 9128

You could create a callback for when the data is loaded, and a variable for the number of times it has been called. Then when the number of calls equals the number of items that must be in the dataset, you are ready to make your visualization:

var itemsLoaded = 0;
function dataLoaded() {
  itemsLoaded++;
  if (itemsLoaded === range.length - 1) {
    createVisualization();
  }
}

Then inside your d3.json callback, you can call dataLoaded after an item has been pushed into totals:

d3.json(url, function(error, data) {
  var total =  {
    'date': after,
    'total': data.total
  };
  totals.push(total);
  dataLoaded();
});

If the order of the elements in totals is really important to you, you might want to use the index explicitly rather than .pushing them into the array:

d3.json(url, function(error, data) {
  var total =  {
    'date': after,
    'total': data.total
  };
  totals[i] = total;
  dataLoaded();
});

Also, be careful with your syntax when evaluating the function for the variable request. You need to make sure you are evaluating the expression first, and then calling it.

Instead of this...

var request = (function(before, after) {/*...*/}(range[i+1], range[i]));

... it should look like this (look at the parens):

var request = (function(before, after) {/*...*/})(range[i+1], range[i]);

Upvotes: 0

Iain Dillingham
Iain Dillingham

Reputation: 625

Here's what I've come up with so far. It works, but I'm sure there must be a better way!

var format = d3.time.format.iso,
    min = format.parse('2014-06-01'),
    max = format.parse('2014-07-01'),
    range = d3.time.day.utc.range(min, max);

totals = [];

for (var i = 0; i < range.length - 1; i++) {

    var request = (function(before, after) {

        var url = 'http://api.crisis.net/item?'
            + '&before=' + format(before)
            + '&after=' + format(after)
            + '&placeName=Syria'
            + '&limit=0'
            + '&apikey=[apikey]';

        d3.json(url, function(error, data) {

            var total =  {

                'date': after,
                'total': data.total

            };

            totals.push(total);

            ready();
        });
    }(range[i + 1], range[i]));
}

function ready() {

    if (totals.length === range.length - 1) {
        // Draw chart here
    }
}

Upvotes: 0

Related Questions